From 18fa0c91cec0e3c185eeb3358e5b25f160e4de05 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Wed, 24 Jan 2018 01:12:06 +0900 Subject: [PATCH 001/161] Reduce Spinach parallelization to 2 nodes and increase RSpec's to 28 nodes --- .gitlab-ci.yml | 120 ++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index be18520b876..2744c7001f9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -323,69 +323,69 @@ setup-test-env: - tmp/tests - config/secrets.yml -rspec-pg 0 27: *rspec-metadata-pg -rspec-pg 1 27: *rspec-metadata-pg -rspec-pg 2 27: *rspec-metadata-pg -rspec-pg 3 27: *rspec-metadata-pg -rspec-pg 4 27: *rspec-metadata-pg -rspec-pg 5 27: *rspec-metadata-pg -rspec-pg 6 27: *rspec-metadata-pg -rspec-pg 7 27: *rspec-metadata-pg -rspec-pg 8 27: *rspec-metadata-pg -rspec-pg 9 27: *rspec-metadata-pg -rspec-pg 10 27: *rspec-metadata-pg -rspec-pg 11 27: *rspec-metadata-pg -rspec-pg 12 27: *rspec-metadata-pg -rspec-pg 13 27: *rspec-metadata-pg -rspec-pg 14 27: *rspec-metadata-pg -rspec-pg 15 27: *rspec-metadata-pg -rspec-pg 16 27: *rspec-metadata-pg -rspec-pg 17 27: *rspec-metadata-pg -rspec-pg 18 27: *rspec-metadata-pg -rspec-pg 19 27: *rspec-metadata-pg -rspec-pg 20 27: *rspec-metadata-pg -rspec-pg 21 27: *rspec-metadata-pg -rspec-pg 22 27: *rspec-metadata-pg -rspec-pg 23 27: *rspec-metadata-pg -rspec-pg 24 27: *rspec-metadata-pg -rspec-pg 25 27: *rspec-metadata-pg -rspec-pg 26 27: *rspec-metadata-pg +rspec-pg 0 28: *rspec-metadata-pg +rspec-pg 1 28: *rspec-metadata-pg +rspec-pg 2 28: *rspec-metadata-pg +rspec-pg 3 28: *rspec-metadata-pg +rspec-pg 4 28: *rspec-metadata-pg +rspec-pg 5 28: *rspec-metadata-pg +rspec-pg 6 28: *rspec-metadata-pg +rspec-pg 7 28: *rspec-metadata-pg +rspec-pg 8 28: *rspec-metadata-pg +rspec-pg 9 28: *rspec-metadata-pg +rspec-pg 10 28: *rspec-metadata-pg +rspec-pg 11 28: *rspec-metadata-pg +rspec-pg 12 28: *rspec-metadata-pg +rspec-pg 13 28: *rspec-metadata-pg +rspec-pg 14 28: *rspec-metadata-pg +rspec-pg 15 28: *rspec-metadata-pg +rspec-pg 16 28: *rspec-metadata-pg +rspec-pg 17 28: *rspec-metadata-pg +rspec-pg 18 28: *rspec-metadata-pg +rspec-pg 19 28: *rspec-metadata-pg +rspec-pg 20 28: *rspec-metadata-pg +rspec-pg 21 28: *rspec-metadata-pg +rspec-pg 22 28: *rspec-metadata-pg +rspec-pg 23 28: *rspec-metadata-pg +rspec-pg 24 28: *rspec-metadata-pg +rspec-pg 25 28: *rspec-metadata-pg +rspec-pg 26 28: *rspec-metadata-pg +rspec-pg 27 28: *rspec-metadata-pg -rspec-mysql 0 27: *rspec-metadata-mysql -rspec-mysql 1 27: *rspec-metadata-mysql -rspec-mysql 2 27: *rspec-metadata-mysql -rspec-mysql 3 27: *rspec-metadata-mysql -rspec-mysql 4 27: *rspec-metadata-mysql -rspec-mysql 5 27: *rspec-metadata-mysql -rspec-mysql 6 27: *rspec-metadata-mysql -rspec-mysql 7 27: *rspec-metadata-mysql -rspec-mysql 8 27: *rspec-metadata-mysql -rspec-mysql 9 27: *rspec-metadata-mysql -rspec-mysql 10 27: *rspec-metadata-mysql -rspec-mysql 11 27: *rspec-metadata-mysql -rspec-mysql 12 27: *rspec-metadata-mysql -rspec-mysql 13 27: *rspec-metadata-mysql -rspec-mysql 14 27: *rspec-metadata-mysql -rspec-mysql 15 27: *rspec-metadata-mysql -rspec-mysql 16 27: *rspec-metadata-mysql -rspec-mysql 17 27: *rspec-metadata-mysql -rspec-mysql 18 27: *rspec-metadata-mysql -rspec-mysql 19 27: *rspec-metadata-mysql -rspec-mysql 20 27: *rspec-metadata-mysql -rspec-mysql 21 27: *rspec-metadata-mysql -rspec-mysql 22 27: *rspec-metadata-mysql -rspec-mysql 23 27: *rspec-metadata-mysql -rspec-mysql 24 27: *rspec-metadata-mysql -rspec-mysql 25 27: *rspec-metadata-mysql -rspec-mysql 26 27: *rspec-metadata-mysql +rspec-mysql 0 28: *rspec-metadata-mysql +rspec-mysql 1 28: *rspec-metadata-mysql +rspec-mysql 2 28: *rspec-metadata-mysql +rspec-mysql 3 28: *rspec-metadata-mysql +rspec-mysql 4 28: *rspec-metadata-mysql +rspec-mysql 5 28: *rspec-metadata-mysql +rspec-mysql 6 28: *rspec-metadata-mysql +rspec-mysql 7 28: *rspec-metadata-mysql +rspec-mysql 8 28: *rspec-metadata-mysql +rspec-mysql 9 28: *rspec-metadata-mysql +rspec-mysql 10 28: *rspec-metadata-mysql +rspec-mysql 11 28: *rspec-metadata-mysql +rspec-mysql 12 28: *rspec-metadata-mysql +rspec-mysql 13 28: *rspec-metadata-mysql +rspec-mysql 14 28: *rspec-metadata-mysql +rspec-mysql 15 28: *rspec-metadata-mysql +rspec-mysql 16 28: *rspec-metadata-mysql +rspec-mysql 17 28: *rspec-metadata-mysql +rspec-mysql 18 28: *rspec-metadata-mysql +rspec-mysql 19 28: *rspec-metadata-mysql +rspec-mysql 20 28: *rspec-metadata-mysql +rspec-mysql 21 28: *rspec-metadata-mysql +rspec-mysql 22 28: *rspec-metadata-mysql +rspec-mysql 23 28: *rspec-metadata-mysql +rspec-mysql 24 28: *rspec-metadata-mysql +rspec-mysql 25 28: *rspec-metadata-mysql +rspec-mysql 26 28: *rspec-metadata-mysql +rspec-mysql 27 28: *rspec-metadata-mysql -spinach-pg 0 3: *spinach-metadata-pg -spinach-pg 1 3: *spinach-metadata-pg -spinach-pg 2 3: *spinach-metadata-pg +spinach-pg 0 2: *spinach-metadata-pg +spinach-pg 1 2: *spinach-metadata-pg -spinach-mysql 0 3: *spinach-metadata-mysql -spinach-mysql 1 3: *spinach-metadata-mysql -spinach-mysql 2 3: *spinach-metadata-mysql +spinach-mysql 0 2: *spinach-metadata-mysql +spinach-mysql 1 2: *spinach-metadata-mysql # Static analysis jobs .ruby-static-analysis: &ruby-static-analysis From 73f999d436b438b61efed262fba70497c40808e9 Mon Sep 17 00:00:00 2001 From: Lars Kumbier Date: Sat, 10 Feb 2018 16:15:15 +0100 Subject: [PATCH 002/161] Fixes an invalid documentation for the .gitlab-ci.yml cache:key --- doc/ci/yaml/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 4a650303d45..80ab63468f2 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -276,7 +276,7 @@ To enable per-branch caching: ```yaml cache: - key: "$CI_COMMIT_REF_NAME" + key: "$CI_COMMIT_REF_SLUG" untracked: true ``` @@ -284,7 +284,7 @@ To enable per-job and per-branch caching: ```yaml cache: - key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" untracked: true ``` @@ -292,7 +292,7 @@ To enable per-branch and per-stage caching: ```yaml cache: - key: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME" + key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG" untracked: true ``` @@ -301,7 +301,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace ```yaml cache: - key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%" + key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_SLUG%" untracked: true ``` @@ -310,7 +310,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace ```yaml cache: - key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME" + key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_SLUG" untracked: true ``` From d9763cb4dfb2eaf78718abc21434a3fde20a4fb6 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 7 Feb 2018 15:39:54 -0500 Subject: [PATCH 003/161] Refactor issue index filtered search --- .../javascripts/pages/search/init_filtered_search.js | 11 +++++++++++ app/views/projects/issues/index.html.haml | 1 - 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js index 250f9d992ab..82ea239bcff 100644 --- a/app/assets/javascripts/pages/search/init_filtered_search.js +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -1,3 +1,14 @@ +import '~/filtered_search/dropdown_emoji'; +import '~/filtered_search/dropdown_hint'; +import '~/filtered_search/dropdown_non_user'; +import '~/filtered_search/dropdown_user'; +import '~/filtered_search/dropdown_utils'; +import '~/filtered_search/filtered_search_dropdown_manager'; +import '~/filtered_search/filtered_search_dropdown'; +import '~/filtered_search/filtered_search_manager'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_visual_tokens'; + export default ({ page }) => { const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); if (filteredSearchEnabled) { diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 193111b4cee..fb06ba58c27 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,7 +6,6 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'filtered_search' = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") From e4990b66df64f2e23502d161f411335c9a771a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 26 Jan 2018 15:23:46 +0100 Subject: [PATCH 004/161] Combine all rake tasks in the static-analysis job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/tasks/lint.rake | 28 ++++++++++++++++++++++++++++ scripts/static-analysis | 7 +------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index 3ab406eff2c..e7812ff3568 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -16,5 +16,33 @@ unless Rails.env.production? task :javascript do Rake::Task['eslint'].invoke end + + desc "GitLab | lint | Run several lint checks" + task :all do + status = 0 + original_stdout = $stdout + + %w[ + config_lint + haml_lint + scss_lint + flay + gettext:lint + lint:static_verification + ].each do |task| + begin + $stdout = StringIO.new + Rake::Task[task].invoke + rescue RuntimeError, SystemExit => ex + raise ex if ex.is_a?(RuntimeError) && task != 'haml_lint' + original_stdout << $stdout.string + status = 1 + ensure + $stdout = original_stdout + end + end + + exit status + end end end diff --git a/scripts/static-analysis b/scripts/static-analysis index bdb88f3cb57..db4df4ee6cb 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -26,15 +26,10 @@ def emit_errors(static_analysis) end tasks = [ - %w[bundle exec rake config_lint], - %w[bundle exec rake flay], - %w[bundle exec rake haml_lint], - %w[bundle exec rake scss_lint], + %w[bin/rake lint:all], %w[bundle exec license_finder], %w[yarn run eslint], %w[bundle exec rubocop --parallel], - %w[bundle exec rake gettext:lint], - %w[bundle exec rake lint:static_verification], %w[scripts/lint-conflicts.sh], %w[scripts/lint-rugged] ] From 4512ed80fc229a8048a950070fa2174754707bc0 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Sun, 28 Jan 2018 19:46:28 +0200 Subject: [PATCH 005/161] Fix new project path input overlapping - Max-width and height --- app/assets/stylesheets/framework/forms.scss | 2 +- app/assets/stylesheets/framework/selects.scss | 2 +- .../unreleased/fix-new-project-path-input-overlapping.yml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/fix-new-project-path-input-overlapping.yml diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index be96c8ee964..f48fe6880cf 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -167,7 +167,7 @@ label { .input-group { .select2-container { display: table-cell; - width: 200px !important; + max-width: 180px; } .input-group-addon { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index dbee7073975..b40dcf93969 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -8,7 +8,7 @@ .select2-choice { background: $white-light; border-color: $input-border; - height: 35px; + height: 34px; padding: $gl-vert-padding $gl-input-padding; font-size: $gl-font-size; line-height: 1.42857143; diff --git a/changelogs/unreleased/fix-new-project-path-input-overlapping.yml b/changelogs/unreleased/fix-new-project-path-input-overlapping.yml new file mode 100644 index 00000000000..fb33ce9437a --- /dev/null +++ b/changelogs/unreleased/fix-new-project-path-input-overlapping.yml @@ -0,0 +1,5 @@ +--- +title: Fix new project path input overlapping +merge_request: 16755 +author: George Tsiolis +type: fixed From 2f0d2ab55b6deac79f81834f6724a676ceae94ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 12 Feb 2018 18:34:07 +0100 Subject: [PATCH 006/161] Run lint:all tasks in forks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/tasks/lint.rake | 43 ++++++++++++++++++++++++++++++----------- scripts/static-analysis | 4 ++-- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index e7812ff3568..fe5032cae18 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -20,7 +20,6 @@ unless Rails.env.production? desc "GitLab | lint | Run several lint checks" task :all do status = 0 - original_stdout = $stdout %w[ config_lint @@ -30,19 +29,41 @@ unless Rails.env.production? gettext:lint lint:static_verification ].each do |task| - begin - $stdout = StringIO.new - Rake::Task[task].invoke - rescue RuntimeError, SystemExit => ex - raise ex if ex.is_a?(RuntimeError) && task != 'haml_lint' - original_stdout << $stdout.string - status = 1 - ensure - $stdout = original_stdout + pid = Process.fork do + rd, wr = IO.pipe + stdout = $stdout.dup + stderr = $stderr.dup + $stdout.reopen(wr) + $stderr.reopen(wr) + + begin + begin + Rake::Task[task].invoke + rescue RuntimeError # The haml_lint tasks raise a RuntimeError + exit(1) + end + rescue SystemExit => ex + msg = "*** Rake task #{task} failed with the following error(s):" + raise ex + ensure + $stdout.reopen(stdout) + $stderr.reopen(stderr) + wr.close + + if msg + warn "\n#{msg}\n\n" + IO.copy_stream(rd, $stderr) + else + IO.copy_stream(rd, $stdout) + end + end end + + Process.waitpid(pid) + status += $?.exitstatus end - exit status + exit(status) end end end diff --git a/scripts/static-analysis b/scripts/static-analysis index db4df4ee6cb..0e67eabfec1 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -7,7 +7,7 @@ require_relative '../lib/gitlab/popen/runner' def emit_warnings(static_analysis) static_analysis.warned_results.each do |result| puts - puts "**** #{result.cmd.join(' ')} had the following warnings:" + puts "**** #{result.cmd.join(' ')} had the following warning(s):" puts puts result.stderr puts @@ -17,7 +17,7 @@ end def emit_errors(static_analysis) static_analysis.failed_results.each do |result| puts - puts "**** #{result.cmd.join(' ')} failed with the following error:" + puts "**** #{result.cmd.join(' ')} failed with the following error(s):" puts puts result.stdout puts result.stderr From 6220c595c69b36be39e12f984b28c35ac97197e4 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Thu, 8 Feb 2018 14:03:20 +0200 Subject: [PATCH 007/161] Apply new default and inline label design --- app/assets/stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/events.scss | 1 - app/assets/stylesheets/pages/issuable.scss | 3 +-- app/assets/stylesheets/pages/labels.scss | 12 ++++++++---- app/assets/stylesheets/pages/todos.scss | 1 - .../fix-change-event-body-label-font-size.yml | 5 +++++ 6 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/fix-change-event-body-label-font-size.yml diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 25ee081ea9c..54e13f9d95c 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -558,6 +558,7 @@ $jq-ui-default-color: #777; /* * Label */ +$label-font-size: 12px; $label-padding: 7px; $label-padding-modal: 10px; $label-gray-bg: #f8fafc; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index da096354b5a..8871a069d5d 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -72,7 +72,6 @@ .label { color: $gl-text-color; - font-size: inherit; } iframe.twitter-share-button { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 759719a72da..2ee932ae44b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -102,12 +102,11 @@ .issuable-show-labels { a { - margin-right: 5px; margin-bottom: 5px; display: inline-block; .color-label { - padding: 6px 10px; + padding: 4px $grid-size; border-radius: $label-border-radius; } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index a72e654824e..0f49d15203b 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -105,13 +105,16 @@ } .label { - padding: 8px 12px; - font-size: 14px; + padding: 4px $grid-size; + font-size: $label-font-size; + position: relative; + top: ($grid-size / 2); } } .color-label { - padding: 3px $label-padding; + padding: 0 $grid-size; + line-height: 16px; border-radius: $label-border-radius; } @@ -302,10 +305,11 @@ } .label-link { - display: inline-block; + display: inline-flex; vertical-align: top; .label { vertical-align: inherit; + font-size: $label-font-size; } } diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index a79772ea37b..4b9824fab0c 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -128,7 +128,6 @@ .label { color: $gl-text-color; - font-size: inherit; } p { diff --git a/changelogs/unreleased/fix-change-event-body-label-font-size.yml b/changelogs/unreleased/fix-change-event-body-label-font-size.yml new file mode 100644 index 00000000000..3192a7bff92 --- /dev/null +++ b/changelogs/unreleased/fix-change-event-body-label-font-size.yml @@ -0,0 +1,5 @@ +--- +title: Apply new default and inline label design +merge_request: 16956 +author: George Tsiolis +type: changed From 75fd83245450216b0aeec9993455802764eaf87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Thu, 15 Feb 2018 09:50:19 -0500 Subject: [PATCH 008/161] Revert "Merge branch 'rd-43185-revert-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key' into 'master'" This reverts commit e607fd796657afd214b8f25201919d3e33b3f35f. --- app/models/key.rb | 7 ++- ...k-spaces-used-when-uploading-a-ssh-key.yml | 5 ++ lib/gitlab/ssh_public_key.rb | 28 +++++++--- spec/factories/keys.rb | 4 ++ spec/lib/gitlab/ssh_public_key_spec.rb | 35 +++++++++++++ spec/models/key_spec.rb | 51 ++++++++++++++++--- 6 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml diff --git a/app/models/key.rb b/app/models/key.rb index 7406c98c99e..ae5769c0627 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -33,9 +33,8 @@ class Key < ActiveRecord::Base after_destroy :refresh_user_cache def key=(value) - value&.delete!("\n\r") - value.strip! unless value.blank? - write_attribute(:key, value) + write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil) + @public_key = nil end @@ -97,7 +96,7 @@ class Key < ActiveRecord::Base def generate_fingerprint self.fingerprint = nil - return unless self.key.present? + return unless public_key.valid? self.fingerprint = public_key.fingerprint end diff --git a/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml b/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml new file mode 100644 index 00000000000..9e4811ca308 --- /dev/null +++ b/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml @@ -0,0 +1,5 @@ +--- +title: Sanitize extra blank spaces used when uploading a SSH key +merge_request: 40552 +author: +type: fixed diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb index 89ca1298120..545e7c74f7e 100644 --- a/lib/gitlab/ssh_public_key.rb +++ b/lib/gitlab/ssh_public_key.rb @@ -21,6 +21,22 @@ module Gitlab technology(name)&.supported_sizes end + def self.sanitize(key_content) + ssh_type, *parts = key_content.strip.split + + return key_content if parts.empty? + + parts.each_with_object("#{ssh_type} ").with_index do |(part, content), index| + content << part + + if Gitlab::SSHPublicKey.new(content).valid? + break [content, parts[index + 1]].compact.join(' ') # Add the comment part if present + elsif parts.size == index + 1 # return original content if we've reached the last element + break key_content + end + end + end + attr_reader :key_text, :key # Unqualified MD5 fingerprint for compatibility @@ -37,23 +53,23 @@ module Gitlab end def valid? - key.present? + key.present? && bits && technology.supported_sizes.include?(bits) end def type - technology.name if valid? + technology.name if key.present? end def bits - return unless valid? + return if key.blank? case type when :rsa - key.n.num_bits + key.n&.num_bits when :dsa - key.p.num_bits + key.p&.num_bits when :ecdsa - key.group.order.num_bits + key.group.order&.num_bits when :ed25519 256 else diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index f0c43f3d6f5..23a98a899f1 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -5,6 +5,10 @@ FactoryBot.define do title key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' } + factory :key_without_comment do + key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate } + end + factory :deploy_key, class: 'DeployKey' factory :personal_key do diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb index 93d538141ce..c15e29774b6 100644 --- a/spec/lib/gitlab/ssh_public_key_spec.rb +++ b/spec/lib/gitlab/ssh_public_key_spec.rb @@ -37,6 +37,41 @@ describe Gitlab::SSHPublicKey, lib: true do end end + describe '.sanitize(key_content)' do + let(:content) { build(:key).key } + + context 'when key has blank space characters' do + it 'removes the extra blank space characters' do + unsanitized = content.insert(100, "\n") + .insert(40, "\r\n") + .insert(30, ' ') + + sanitized = described_class.sanitize(unsanitized) + _, body = sanitized.split + + expect(sanitized).not_to eq(unsanitized) + expect(body).not_to match(/\s/) + end + end + + context "when key doesn't have blank space characters" do + it "doesn't modify the content" do + sanitized = described_class.sanitize(content) + + expect(sanitized).to eq(content) + end + end + + context "when key is invalid" do + it 'returns the original content' do + unsanitized = "ssh-foo any content==" + sanitized = described_class.sanitize(unsanitized) + + expect(sanitized).to eq(unsanitized) + end + end + end + describe '#valid?' do subject { public_key } diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 7398fd25aa8..bf5703ac986 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -72,16 +72,53 @@ describe Key, :mailer do expect(build(:key)).to be_valid end - it 'accepts a key with newline charecters after stripping them' do - key = build(:key) - key.key = key.key.insert(100, "\n") - key.key = key.key.insert(40, "\r\n") - expect(key).to be_valid - end - it 'rejects the unfingerprintable key (not a key)' do expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end + + where(:factory, :chars, :expected_sections) do + [ + [:key, ["\n", "\r\n"], 3], + [:key, [' ', ' '], 3], + [:key_without_comment, [' ', ' '], 2] + ] + end + + with_them do + let!(:key) { create(factory) } + let!(:original_fingerprint) { key.fingerprint } + + it 'accepts a key with blank space characters after stripping them' do + modified_key = key.key.insert(100, chars.first).insert(40, chars.last) + _, content = modified_key.split + + key.update!(key: modified_key) + + expect(key).to be_valid + expect(key.key.split.size).to eq(expected_sections) + + expect(content).not_to match(/\s/) + expect(original_fingerprint).to eq(key.fingerprint) + end + end + end + + context 'validate size' do + where(:key_content, :result) do + [ + [Spec::Support::Helpers::KeyGeneratorHelper.new(512).generate, false], + [Spec::Support::Helpers::KeyGeneratorHelper.new(8192).generate, false], + [Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate, true] + ] + end + + with_them do + it 'validates the size of the key' do + key = build(:key, key: key_content) + + expect(key.valid?).to eq(result) + end + end end context 'validate it meets key restrictions' do From 27a6d65c616b58fdce5d7eba7a11b7ef7b2c8d28 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Feb 2018 13:44:55 -0600 Subject: [PATCH 009/161] Add dispatcher imports for job details bundle --- app/assets/javascripts/dispatcher.js | 5 +++++ app/assets/javascripts/jobs/job_details_bundle.js | 4 ++-- app/assets/javascripts/pages/projects/jobs/show/index.js | 3 +++ app/views/projects/jobs/show.html.haml | 4 ---- config/webpack.config.js | 4 +--- 5 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/pages/projects/jobs/show/index.js diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index e4288dc1317..2e5209c6bf1 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -192,6 +192,11 @@ var Dispatcher; .catch(fail); shortcut_handler = true; break; + case 'projects:jobs:show': + import('./pages/projects/jobs/show') + .then(callDefault) + .catch(fail); + break; case 'projects:merge_requests:creations:new': import('./pages/projects/merge_requests/creations/new') .then(callDefault) diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index db53b04de0e..85a88ae409b 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -3,7 +3,7 @@ import JobMediator from './job_details_mediator'; import jobHeader from './components/header.vue'; import detailsBlock from './components/sidebar_details_block.vue'; -document.addEventListener('DOMContentLoaded', () => { +export default () => { const dataset = document.getElementById('js-job-details-vue').dataset; const mediator = new JobMediator({ endpoint: dataset.endpoint }); @@ -55,4 +55,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +}; diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js new file mode 100644 index 00000000000..cecbfb82946 --- /dev/null +++ b/app/assets/javascripts/pages/projects/jobs/show/index.js @@ -0,0 +1,3 @@ +import initJobDetails from '~/jobs/job_details_bundle'; + +export default initJobDetails; diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 93efa7e8e86..849c273db8c 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -112,7 +112,3 @@ .js-build-options{ data: javascript_build_options } #js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json) } } - -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('job_details') diff --git a/config/webpack.config.js b/config/webpack.config.js index 3fff808f166..edf5256aadc 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -68,7 +68,6 @@ var config = { help: './help/help.js', how_to_merge: './how_to_merge.js', issue_show: './issue_show/index.js', - job_details: './jobs/job_details_bundle.js', locale: './locale/index.js', main: './main.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', @@ -155,7 +154,7 @@ var config = { include: /node_modules\/katex\/dist/, use: [ { loader: 'style-loader' }, - { + { loader: 'css-loader', options: { name: '[name].[hash].[ext]' @@ -263,7 +262,6 @@ var config = { 'filtered_search', 'groups', 'issue_show', - 'job_details', 'merge_conflicts', 'monitoring', 'notebook_viewer', From e5c8a47cbb39dddb0855c1ddfa505f083fff0c78 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 8 Feb 2018 15:36:11 -0600 Subject: [PATCH 010/161] fix monaco editor config to ensure service workers are not loaded from cross-domain origins --- app/assets/javascripts/ide/monaco_loader.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js index af83a1ec0b4..142a220097b 100644 --- a/app/assets/javascripts/ide/monaco_loader.js +++ b/app/assets/javascripts/ide/monaco_loader.js @@ -6,6 +6,11 @@ monacoContext.require.config({ }, }); +// ignore CDN config and use local assets path for service worker which cannot be cross-domain +const relativeRootPath = (gon && gon.relative_url_root) || ''; +const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`; +window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` }; + // eslint-disable-next-line no-underscore-dangle window.__monaco_context__ = monacoContext; export default monacoContext.require; From b49d5dd11eda9e1213e0f3c87aab863924fe16cc Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 8 Feb 2018 15:39:51 -0600 Subject: [PATCH 011/161] add CHANGELOG.md entry for !17021 --- ...1-monaco-service-workers-do-not-work-with-cdn-enabled.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/42641-monaco-service-workers-do-not-work-with-cdn-enabled.yml diff --git a/changelogs/unreleased/42641-monaco-service-workers-do-not-work-with-cdn-enabled.yml b/changelogs/unreleased/42641-monaco-service-workers-do-not-work-with-cdn-enabled.yml new file mode 100644 index 00000000000..955a5a27e21 --- /dev/null +++ b/changelogs/unreleased/42641-monaco-service-workers-do-not-work-with-cdn-enabled.yml @@ -0,0 +1,5 @@ +--- +title: Fix monaco editor features which were incompatable with GitLab CDN settings +merge_request: 17021 +author: +type: fixed From 1c2d283d506f2ceb4777622837fffe381e34cb9d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Feb 2018 16:50:20 -0600 Subject: [PATCH 012/161] Add dispatcher imports for bundles used in merge request show path --- .../diff_notes/diff_notes_bundle.js | 4 +-- app/assets/javascripts/how_to_merge.js | 25 ++++++++++--------- .../projects/merge_requests/show/index.js | 8 ++++-- .../merge_requests/_how_to_merge.html.haml | 3 --- .../projects/merge_requests/show.html.haml | 3 --- config/webpack.config.js | 3 +-- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index e0422057090..38c42a11b4e 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -15,7 +15,7 @@ import './components/resolve_discussion_btn'; import './components/diff_note_avatars'; import './components/new_issue_for_discussion'; -$(() => { +export default () => { const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box'); const projectPath = projectPathHolder.dataset.projectPath; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; @@ -75,4 +75,4 @@ $(() => { }); $(window).trigger('resize.nav'); -}); +}; diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js index 19f4a946f73..12e6f24595a 100644 --- a/app/assets/javascripts/how_to_merge.js +++ b/app/assets/javascripts/how_to_merge.js @@ -1,12 +1,13 @@ -document.addEventListener('DOMContentLoaded', () => { - const modal = $('#modal_merge_info').modal({ - modal: true, - show: false, - }); - $('.how_to_merge_link').on('click', () => { - modal.show(); - }); - $('.modal-header .close').on('click', () => { - modal.hide(); - }); -}); +export default () => { + const modal = $('#modal_merge_info'); + + if (modal) { + modal.modal({ + modal: true, + show: false, + }); + + $('.how_to_merge_link').on('click', modal.show); + $('.modal-header .close').on('click', modal.hide); + } +}; diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index c3463c266e3..8552e49a9cf 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -2,16 +2,19 @@ import MergeRequest from '~/merge_request'; import ZenMode from '~/zen_mode'; import initNotes from '~/init_notes'; import initIssuableSidebar from '~/init_issuable_sidebar'; +import initDiffNotes from '~/diff_notes/diff_notes_bundle'; import ShortcutsIssuable from '~/shortcuts_issuable'; import Diff from '~/diff'; import { handleLocationHash } from '~/lib/utils/common_utils'; +import howToMerge from '~/how_to_merge'; export default () => { new Diff(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new - initIssuableSidebar(); // eslint-disable-line no-new - initNotes(); // eslint-disable-line no-new + initIssuableSidebar(); + initNotes(); + initDiffNotes(); const mrShowNode = document.querySelector('.merge-request'); @@ -21,4 +24,5 @@ export default () => { new ShortcutsIssuable(true); // eslint-disable-line no-new handleLocationHash(); + howToMerge(); }; diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 917ec7fdbda..54a661040ea 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -1,6 +1,3 @@ -- content_for :page_specific_javascripts do - = webpack_bundle_tag('how_to_merge') - #modal_merge_info.modal .modal-dialog .modal-content diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 8740c6895df..b0fa6c2bd7b 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -4,9 +4,6 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('diff_notes') .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } } = render "projects/merge_requests/mr_title" diff --git a/config/webpack.config.js b/config/webpack.config.js index 3fff808f166..b31529591e9 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -66,7 +66,6 @@ var config = { graphs_charts: './graphs/graphs_charts.js', graphs_show: './graphs/graphs_show.js', help: './help/help.js', - how_to_merge: './how_to_merge.js', issue_show: './issue_show/index.js', job_details: './jobs/job_details_bundle.js', locale: './locale/index.js', @@ -155,7 +154,7 @@ var config = { include: /node_modules\/katex\/dist/, use: [ { loader: 'style-loader' }, - { + { loader: 'css-loader', options: { name: '[name].[hash].[ext]' From 092c56018a4357776aad1192bfb542af3eedcde0 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Fri, 16 Feb 2018 15:20:16 +0500 Subject: [PATCH 013/161] Move spinach project network graph tests to RSpec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- features/project/network_graph.feature | 46 -------- features/steps/project/network_graph.rb | 116 ------------------- spec/features/projects/network_graph_spec.rb | 108 +++++++++++++++++ 3 files changed, 108 insertions(+), 162 deletions(-) delete mode 100644 features/project/network_graph.feature delete mode 100644 features/steps/project/network_graph.rb create mode 100644 spec/features/projects/network_graph_spec.rb diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature deleted file mode 100644 index 93c884e23c5..00000000000 --- a/features/project/network_graph.feature +++ /dev/null @@ -1,46 +0,0 @@ -Feature: Project Network Graph - Background: - Given I sign in as a user - And I own project "Shop" - And I visit project "Shop" network page - - @javascript - Scenario: I should see project network - Then page should have network graph - And page should select "master" in select box - And page should have "master" on graph - - @javascript - Scenario: I should see project network with 'test' branch - When I visit project network page on branch 'test' - Then page should have 'test' on graph - - @javascript - Scenario: I should switch "branch" and "tag" - When I switch ref to "feature" - Then page should select "feature" in select box - And page should have "feature" on graph - When I switch ref to "v1.0.0" - Then page should select "v1.0.0" in select box - And page should have "v1.0.0" on graph - - @javascript - Scenario: I should looking for a commit by SHA - When I looking for a commit by SHA of "v1.0.0" - Then page should have network graph - And page should select "master" in select box - And page should have "v1.0.0" on graph - - @javascript - Scenario: I should filter selected tag - When I switch ref to "v1.0.0" - Then page should have "v1.0.0" in title - Then page should have content not containing "v1.0.0" - When click "Show only selected branch" checkbox - Then page should only have content from "v1.0.0" - When click "Show only selected branch" checkbox - Then page should have content not containing "v1.0.0" - - Scenario: I should fail to look for a commit - When I look for a commit by ";" - Then I should see non-existent git revision error message diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb deleted file mode 100644 index ba98d861e7b..00000000000 --- a/features/steps/project/network_graph.rb +++ /dev/null @@ -1,116 +0,0 @@ -class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'page should have network graph' do - expect(page).to have_selector ".network-graph" - end - - When 'I visit project "Shop" network page' do - # Stub Graph max_size to speed up test (10 commits vs. 650) - Network::Graph.stub(max_count: 10) - - @project = Project.find_by(name: "Shop") - visit project_network_path(@project, "master") - end - - step "I visit project network page on branch 'test'" do - visit project_network_path(@project, "'test'") - end - - step 'page should select "master" in select box' do - expect(page).to have_selector '.dropdown-menu-toggle', text: "master" - end - - step 'page should select "v1.0.0" in select box' do - expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0" - end - - step 'page should have "master" on graph' do - page.within '.network-graph' do - expect(page).to have_content 'master' - end - end - - step "page should have 'test' on graph" do - page.within '.network-graph' do - expect(page).to have_content "'test'" - end - end - - When 'I switch ref to "feature"' do - first('.js-project-refs-dropdown').click - - page.within '.project-refs-form' do - click_link 'feature' - end - end - - When 'I switch ref to "v1.0.0"' do - first('.js-project-refs-dropdown').click - - page.within '.project-refs-form' do - click_link 'v1.0.0' - end - end - - When 'click "Show only selected branch" checkbox' do - find('#filter_ref').click - end - - step 'page should have content not containing "v1.0.0"' do - page.within '.network-graph' do - expect(page).to have_content 'Change some files' - end - end - - step 'page should have "v1.0.0" in title' do - expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false - end - - step 'page should only have content from "v1.0.0"' do - page.within '.network-graph' do - expect(page).not_to have_content 'Change some files' - end - end - - step 'page should select "feature" in select box' do - expect(page).to have_selector '.dropdown-menu-toggle', text: "feature" - end - - step 'page should select "v1.0.0" in select box' do - expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0" - end - - step 'page should have "feature" on graph' do - page.within '.network-graph' do - expect(page).to have_content 'feature' - end - end - - When 'I looking for a commit by SHA of "v1.0.0"' do - page.within ".network-form" do - fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' - find('button').click - end - sleep 2 - end - - step 'page should have "v1.0.0" on graph' do - page.within '.network-graph' do - expect(page).to have_content 'v1.0.0' - end - end - - When 'I look for a commit by ";"' do - page.within ".network-form" do - fill_in 'extended_sha1', with: ';' - find('button').click - end - end - - step 'I should see non-existent git revision error message' do - expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist." - end -end diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb new file mode 100644 index 00000000000..9f9a7787093 --- /dev/null +++ b/spec/features/projects/network_graph_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe 'Project Network Graph', :js do + let(:user) { create :user } + let(:project) { create :project, :repository, namespace: user.namespace } + + before do + sign_in(user) + + # Stub Graph max_size to speed up test (10 commits vs. 650) + allow(Network::Graph).to receive(:max_count).and_return(10) + end + + context 'when branch is master' do + def switch_ref_to(ref_name) + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link ref_name + end + end + + def click_show_only_selected_branch_checkbox + find('#filter_ref').click + end + + before do + visit project_network_path(project, 'master') + end + + it 'renders project network' do + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'master' + end + end + + it 'switches ref to branch' do + switch_ref_to('feature') + + expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature' + page.within '.network-graph' do + expect(page).to have_content 'feature' + end + end + + it 'switches ref to tag' do + switch_ref_to('v1.0.0') + + expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0' + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end + end + + it 'renders by commit sha of "v1.0.0"' do + page.within ".network-form" do + fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' + find('button').click + end + + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end + end + + it 'filters select tag' do + switch_ref_to('v1.0.0') + + expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end + + click_show_only_selected_branch_checkbox + + page.within '.network-graph' do + expect(page).not_to have_content 'Change some files' + end + + click_show_only_selected_branch_checkbox + + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end + end + + it 'renders error message when sha commit not exists' do + page.within ".network-form" do + fill_in 'extended_sha1', with: ';' + find('button').click + end + + expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist." + end + end + + it 'renders project network with test branch' do + visit project_network_path(project, "'test'") + + page.within '.network-graph' do + expect(page).to have_content "'test'" + end + end +end From a69b36264b91223934e23184175ad8f9622465fa Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 16 Feb 2018 12:00:13 +0100 Subject: [PATCH 014/161] Fix and simplify end-to-end tests for secret variables --- qa/qa/factory/resource/secret_variable.rb | 12 --------- qa/qa/page/base.rb | 4 +++ .../page/project/settings/secret_variables.rb | 26 +++++++++---------- .../project/add_secret_variable_spec.rb | 19 +++++++++----- qa/spec/page/base_spec.rb | 11 ++++---- 5 files changed, 33 insertions(+), 39 deletions(-) diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb index af0fa8af2df..c734d739b4a 100644 --- a/qa/qa/factory/resource/secret_variable.rb +++ b/qa/qa/factory/resource/secret_variable.rb @@ -4,18 +4,6 @@ module QA class SecretVariable < Factory::Base attr_accessor :key, :value - product :key do - Page::Project::Settings::CICD.act do - expand_secret_variables(&:variable_key) - end - end - - product :value do - Page::Project::Settings::CICD.act do - expand_secret_variables(&:variable_value) - end - end - dependency Factory::Resource::Project, as: :project do |project| project.name = 'project-with-secret-variables' project.description = 'project for adding secret variable test' diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 7924479e2a1..a313d46205d 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -98,6 +98,10 @@ module QA views.map(&:errors).flatten end + def self.elements + views.map(&:elements).flatten + end + class DSL attr_reader :views diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/secret_variables.rb index fea4acb389a..c95c79f137d 100644 --- a/qa/qa/page/project/settings/secret_variables.rb +++ b/qa/qa/page/project/settings/secret_variables.rb @@ -6,39 +6,37 @@ module QA include Common view 'app/views/ci/variables/_variable_row.html.haml' do + element :variable_row, '.ci-variable-row-body' element :variable_key, '.js-ci-variable-input-key' element :variable_value, '.js-ci-variable-input-value' + element :key_placeholder, 'Input variable key' + element :value_placeholder, 'Input variable value' end view 'app/views/ci/variables/_index.html.haml' do element :save_variables, '.js-secret-variables-save-button' + element :reveal_values, '.js-secret-value-reveal-button' end def fill_variable_key(key) - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do - page.find('.js-ci-variable-input-key').set(key) - end + fill_in('Input variable key', with: key, match: :first) end def fill_variable_value(value) - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do - page.find('.js-ci-variable-input-value').set(value) - end + fill_in('Input variable value', with: value, match: :first) end def save_variables - click_button('Save variables') + find('.js-secret-variables-save-button').click end - def variable_key - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do - page.find('.js-ci-variable-input-key').value - end + def reveal_variables + find('.js-secret-value-reveal-button').click end - def variable_value - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do - page.find('.js-ci-variable-input-value').value + def variable_value(key) + within('.ci-variable-row-body', text: key) do + find('.js-ci-variable-input-value').value end end end diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb index 36422a92afc..460f39505f6 100644 --- a/qa/qa/specs/features/project/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb @@ -4,16 +4,21 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - variable_key = 'VARIABLE_KEY' - variable_value = 'variable value' - variable = Factory::Resource::SecretVariable.fabricate! do |resource| - resource.key = variable_key - resource.value = variable_value + resource.key = 'VARIABLE_KEY' + resource.value = 'some secret variable' end - expect(variable.key).to eq(variable_key) - expect(variable.value).to eq(variable_value) + Page::Project::Settings::CICD.perform do |settings| + settings.expand_secret_variables do |page| + expect(page).to have_field(with: 'VARIABLE_KEY') + expect(page).not_to have_field(with: 'some secret variable') + + page.reveal_variables + + expect(page).to have_field(with: 'some secret variable') + end + end end end end diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 287adf35c46..52daa9697ee 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -14,7 +14,7 @@ describe QA::Page::Base do end view 'path/to/some/_partial.html.haml' do - element :something, 'string pattern' + element :another_element, 'string pattern' end end end @@ -25,11 +25,10 @@ describe QA::Page::Base do end it 'populates views objects with data about elements' do - subject.views.first.elements.tap do |elements| - expect(elements.size).to eq 2 - expect(elements).to all(be_an_instance_of QA::Page::Element) - expect(elements.map(&:name)).to eq [:something, :something_else] - end + expect(subject.elements.size).to eq 3 + expect(subject.elements).to all(be_an_instance_of QA::Page::Element) + expect(subject.elements.map(&:name)) + .to eq [:something, :something_else, :another_element] end end From 8469725ec4a43b24f825017c5447529f9b4a2cc8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 16 Feb 2018 11:10:34 +0000 Subject: [PATCH 015/161] Show loading button inline in MR widget --- .../components/states/mr_widget_auto_merge_failed.vue | 5 ++++- changelogs/unreleased/fl-refresh-btn.yml | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/fl-refresh-btn.yml diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 40c3cb500bb..ebaf2b972eb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -44,7 +44,10 @@ type="button" class="btn btn-xs btn-default" > - + {{ s__("mrWidget|Refresh") }} diff --git a/changelogs/unreleased/fl-refresh-btn.yml b/changelogs/unreleased/fl-refresh-btn.yml new file mode 100644 index 00000000000..640fdda9ce7 --- /dev/null +++ b/changelogs/unreleased/fl-refresh-btn.yml @@ -0,0 +1,5 @@ +--- +title: Show loading button inline in refresh button in MR widget +merge_request: +author: +type: fixed From 2b64c67c1f563f805e53b14777452382b0c7e04a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 16 Feb 2018 13:13:59 +0100 Subject: [PATCH 016/161] Remove useless assignment in secret variables specs --- qa/qa/specs/features/project/add_secret_variable_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb index 460f39505f6..d1bf7849bd0 100644 --- a/qa/qa/specs/features/project/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb @@ -4,7 +4,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - variable = Factory::Resource::SecretVariable.fabricate! do |resource| + Factory::Resource::SecretVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' resource.value = 'some secret variable' end From 7044a3a54a4ee4dd45af111727df2ff512db1a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 16 Feb 2018 11:32:08 -0500 Subject: [PATCH 017/161] Validate SSH keys through the sshkey gem --- Gemfile | 1 + Gemfile.lock | 2 + lib/gitlab/ssh_public_key.rb | 2 +- spec/factories/keys.rb | 112 ++++++++++++++++++++----- spec/lib/gitlab/ssh_public_key_spec.rb | 26 +++++- spec/models/key_spec.rb | 21 +---- 6 files changed, 119 insertions(+), 45 deletions(-) diff --git a/Gemfile b/Gemfile index 880ed483c34..a05ca23f5eb 100644 --- a/Gemfile +++ b/Gemfile @@ -401,6 +401,7 @@ gem 'sys-filesystem', '~> 1.1.6' # SSH host key support gem 'net-ssh', '~> 4.1.0' +gem 'sshkey', '~> 1.9.0' # Required for ED25519 SSH host key support group :ed25519 do diff --git a/Gemfile.lock b/Gemfile.lock index 22c4fc0ef28..8de6c8d80a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -895,6 +895,7 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) sqlite3 (1.3.13) + sshkey (1.9.0) stackprof (0.2.10) state_machines (0.4.0) state_machines-activemodel (0.4.0) @@ -1192,6 +1193,7 @@ DEPENDENCIES spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.1.0) sprockets (~> 3.7.0) + sshkey (~> 1.9.0) stackprof (~> 0.2.10) state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb index 545e7c74f7e..6f63ea91ae8 100644 --- a/lib/gitlab/ssh_public_key.rb +++ b/lib/gitlab/ssh_public_key.rb @@ -53,7 +53,7 @@ module Gitlab end def valid? - key.present? && bits && technology.supported_sizes.include?(bits) + SSHKey.valid_ssh_public_key?(key_text) end def type diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index 23a98a899f1..3f0c60f32b7 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -22,38 +22,104 @@ FactoryBot.define do factory :rsa_key_2048 do key do <<~KEY.delete("\n") - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9 - 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5 - /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7 - M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC - rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0 - 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC98dbu7gxcbmAvwMqz/6AALhSr1jiX + G0UC8FQMvoDt+ciB+uSJhg7KlxinKjYJnPGfhX+q2K+mmCGAmI/D6q7rFxE+bn09O+75 + qgkTHi+suDVE6KG7L3n0alGd/qSevfomR77Snh6fQPdG6sEAZz3kehcpfVnq5/IuLFq9 + FBrgmu52Jd4XZLQZKkDq6zYOJ69FUkGf93LZIV/OOaS+f+qkOGPCUkdKl7oEcgpVNY9S + RjBCduXnvi2CyQnnJVkBguGL5VlXwFXH+17Whs7oFWmdiG+4jzBRLIMz4EuIW09b8Su5 + PW6+bBuXOifHA8KG5TMmjs5LYdCMPFnhTyDyO3a1 dummy@gitlab.com KEY end factory :rsa_deploy_key_2048, class: 'DeployKey' end + factory :rsa_key_4096 do + key do + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGSD77lLtjmzewiBs6nu2R5nu6oNkrA + kH/0co1fHHosKfRr+sWkSTKXOVcL7bhRu+tniGBmB5pn+i1qX7BXtrcnv//bCXWIp+me0 + 27L4RJa5/Ep077iiTJlzTpcV664xNUXC8mzBr601HR/Z2TzX5DWJvnyqqFkN7qHTYo/+I + oKECnKqNzI5SQrAxgi6sbWA5DFQ/nwcqsUSBo5gCCJ/0QPrR19yVV5lJA19EY2LawOb1S + JNOFo4mQupSlBZwvERZJ7IqhBTPtQIfrqqz5VJbI13jK3ViZTugIZqydWAhosUyejP3Sd + Cj1KMexrvV95tjUtmhVFlph4tKThQO0p9pXKZNCzYsbQTye6O6Hk2rojOJLyFWqNBVKtI + 8Ymfu7OQWppRnuUFuhuuS515H1s888bZFMPsC74mPyo0Y7Q9wAoTnQ9Hw6b0J6OfY3PIR + VphaCmxh6b7dgSPFdD7TA6j0xk6PCTOIEzBKuc85B3GQc8Nt4sTv6fW8lGeuYWqepW74i + geC4qB6U3/3+p3nPdq/bTM1txrhnQsl1r4dv6TLZ51EtHp6sXayp0qd0pRaiavebXFC0i + aETLraQpye4FWbBL/8xTjQ/0VPrYVuUCDvDSMIIS3/9g7Kp7ERUDC9jUqOVonm4pTXL9i + ItiUBlK7Mob9C4fQIRFnVR00DCmkmVgw== dummy@gitlab.com + KEY + end + end + + factory :rsa_key_5120 do + key do + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACgQDxnZP0TucLH3zcrvt75DPNq+xKqOmJk + CEzTytKq4S5MDH0nlx+xOZ9WykhwDHXU0iZBJF7yRdLkZweYDJVKnBzr4t7QP5Sw2/ZdL + elvUMWGJjuz28x8Z+8NZ+IxL/exDz7itrhCsLupQhGO1obiIwf8xVzzPoxrQ9dxaN4x96 + 5N+QdQcld8O6xfpSE0p5Y3sRn3kp57aHWoNa/bUGZy0OHLr/ig0uc6EKyWsTmEESOgDyV + 94wOyHR0KNGEENyxQt4BwAbEBn3Y41HKqD358KKh+XjbECebrrBFigdDL/eYFIUlstJ07 + SK/HtYjZbiUZCPs8bJA+SBaLK0pGGqguM2LXRoMeMUZFwKKKS2LpRqjKGj3Qt7qMnp1Sk + VhiMnxNqL4nJnDOOVo07xDIPKqIBYO67/cp4Icv3IjKxy6K3EIpLr+iRCxcllpDogxolz + FC+pEDVpmEvcrGEv1ON6HcCdk/6Q8Iekr8rYDHpKCU5FF2uBHkqq7yNJ1/+NFC4dgyOo0 + xCVL4D3DvDKNxFYkrzW4ICt0f5XcMnU10yS/OFXz8JwA3jvuLvMRe5JdFiIjb/l86+TgY + yvK8Y8N/UWgSgyjXUCv8nxdvpsxdz5h7HBF8E2DIxCVMC23655e5rp5eJW9EU9X5YFZc3 + u6uWJ1f1aO+1ViTtqkPrqxovNDD+gVel8Ny6MJ4MvmDKY+eM8beNMSSf1n1Oyh/SvCffh + ZpUqrXdTr9qwZEOaC75T74AJ7KBl9VvO3vPLZuJrt38R2OZG/4SlNEUA6bb5TWQLtdor/ + qpPN5jAskkAUzOh5L/M+dmq2jNn03U9xwORCYPZj+fFM9bL99/0knsV0ypZDZyWH dummy@gitlab.com + KEY + end + end + + factory :rsa_key_8192 do + key do + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAQC5jMyGtgMOVX4t2GuXkbirJA0Edr+ql + OH9grnRBPHPo0Npt6XE6ZN3J3hDULTQo03wmekGw42dxdNSgk+F0GjsUBrMLbqrk485MM + e0cUbP4lRXNu4ao87wPVM5fAsD4E3FQiZcI6Df011ZGIL7hGTHt6eafTfr9cJheRyYSu6 + g06rlnFWbbtSh9oQ7Y6sfDLBcsC9ECcXwe3mwViuQXPIVomZ02EdnBbAhbGHDtA+ZbSvT + fraxOMjkxkVvvdjLxXEykpwVuZf8eZ+R/Js8jQ5RKvTZMbfxJNsGEqHD32s43ml4VF549 + Qz2GJDXF7Cld/n3CT6wvw0mMPM0LnykL2v0CMr44bjIA3KsNEs5MhkcBO8sv5hGfcPhrp + m9WwI6gd9vdZVcxarVI+iQS947owvdn4VbEZXynCDqEEv3Zh+FA5p23mf2p7DkG/swiK/ + IPrjr1wmsiWmwIUsENzJNyJtibKuRsBawC4ZdL797tFilSoTzSpriegSL13joPXz3eOHC + Vu4ATHMo3QyLfIFbxrf9PQ79nyOpHoX2YeFXvei3xFkGMundkOqeI+pnJKDyqbiLV7UVl + clua11QWNQZf1ZUd0n1wZ1g89de+wl3oJSRbSA5ZpveZEPstcMC/JhogY4JBYsvCT1yHO + oNWHo90NZQsUCjNnR+/FVaACtpt2zcPTjjbXvxwCDlT3gXTmTBp/kEZq6u8p+BOlqFgxc + P/sdAR8jWTin3Iw/YAcbqNgRHdjMUzJBrPQ5NcK6xFcmkOEQahdJDZs98xozCHkD4Urx6 + +auTr/uqRYobKoNUNiYqN1n7/dfZjQJJVkHtKd06JTFx+7/SqyfrTKS+/EIf2Hypdy9r9 + IFR+SWAOi11N/wflS/ZbH95Qt3STifXRecmHzyYGkMOZ+mg3Hi2YU0yn7k+P1jy627xud + pT9Ak3HWT5ji8tMyn9udL7m80dYpUiEAxoYZdbSSNCDaKP4ViABnGIeZreIujabI8IdtE + IjFQTaF2d5HTYjp28/qf576CFP5L7AGydypipYqZUmsYnay5YVjdm89He3TMD71SwspJl + POC4RnM0HS87OE+U0+mVaIe8YYbcjTekpVU9mkqsE/GQ34Egw79VMNNgWq5avOzpT8msC + lTJxgfJ1agGgigTvGxUM0FB07+sIdJxxNymAGpLKZ1op8xaJI3o8D86jWgI22za1zxUB5 + il9U7+KOzaWo9mp3bmhvZWGDwzTXEZhUJYMRby7o6UxSHlA6fKE63JSDD2yhXk4CjsQRN + C7Ph9cYSB+Wa3i9Am4rRlJgrF79okmEOMpj1idliHkpIsy/k2CN9Lf2EIHOD4NMuLrSUH + 4qJsPUq19ZbGIMdImD3vMS5b dummy@gitlab.com + KEY + end + end + factory :dsa_key_2048 do key do <<~KEY.delete("\n") - ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G - Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp - YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ - /pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz - OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv - 5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB - AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t - poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1 - M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH - MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H - nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A - 1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb - aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI - zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex - PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z - wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS - Taja+Cf9kMo== dummy@gitlab.com + ssh-dss AAAAB3NzaC1kc3MAAAEBALEB3sM2kPy6LKLiyL+UlDx2vzuKrzSD2nsW2Kb7 + 0ivIqDNJu5CbqIQSkjdMzJiocs33ESFqXid6ezOtVdDwXHJQRxKGalW1kBbFAPjtMxlD + bf559+7qN2zfCfcQsgTmNAZ7O+wltqJmyLv5i4QqNwPDvyeBvJ4C+770DzlcQtpkflKJ + X+O7i8Ylq34h6UTCTnjry+dFVm1xz97LPf7XuzXGZcAG/eGUNQgxQ2bferKnrpYOXx6c + ocSRj9W54nrRFMWuDeOspWp4MoYK0FRMfDQYPksUayGUnm1KQTGuDbB0ahRNCOm8b3tf + P9Z+vjANAkqenzDuXCpz2PU/Oj6/N/UAAAAhAPOLyut12Mjcp3eUXLe1xSoI5IRXSLso + W9no93dcFNprAAABAQCLhpqKY+PNcwbhhPruL+f+uROghHzDwRNX+e231F4wHHeDDomf + WyLVFj31XrHdDXZnS9tTTj5D2XWLovSSxYb3H7earTctmktL0lQ3HapujzvOkn+VM0pG + s6B3j54+AM3mg50KZdYWxxv+v/lb6oEcsCjfKNyRIx/5pqX6XI3dxl9MMIxrfVWpkNX+ + FI68v1LVV61DC9PkNyEHU0v9YBOfrTiS21TIlVIZcSFhuDjg52MekfZAnoKaP7YFJNF3 + fdCrXaU3hYQrwB9XdskBUppwxKGhf7O6SWEZhAEfPA9kgxaWHoJvsDz8aca576UNe7BP + mjzo/SLUX+P4uvcaffd+AAABAEqzpmwjzTxB+DV8C+0LnmKf3L/UlQWyGdmhd65rnbkH + GgRMAAkoh4GBOEHL5bznNRmO7X/H6g2fR7SEabxfbvb903KI4nbfFF+3QtnwyIbTBAcH + 0893D3bi5rsaJcz+c6lBob2En2nThRciefXUk2oPzCQuDyFIyHLJikqRQVcalHCdQ00c + /H/JkiJedHNqaeU4TeMk8SM53Brjplj/iiJq+ujc5MlEgACdCwWp0BviFACEoYyFaa3R + kc7Xdm9vFpclm9fzgUfPloASA0SkO945in3mIqMfODTb4yRvbjk8If9483fEPgQkczpd + ptBz1VAKg8AmRcz1GmBIxs+Stn0= dummy@gitlab.com KEY end end diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb index c15e29774b6..a6ea07e8b6d 100644 --- a/spec/lib/gitlab/ssh_public_key_spec.rb +++ b/spec/lib/gitlab/ssh_public_key_spec.rb @@ -76,7 +76,21 @@ describe Gitlab::SSHPublicKey, lib: true do subject { public_key } context 'with a valid SSH key' do - it { is_expected.to be_valid } + where(:factory) do + %i(rsa_key_2048 + rsa_key_4096 + rsa_key_5120 + rsa_key_8192 + dsa_key_2048 + ecdsa_key_256 + ed25519_key_256) + end + + with_them do + let(:key) { attributes_for(factory)[:key] } + + it { is_expected.to be_valid } + end end context 'with an invalid SSH key' do @@ -117,6 +131,9 @@ describe Gitlab::SSHPublicKey, lib: true do where(:factory, :bits) do [ [:rsa_key_2048, 2048], + [:rsa_key_4096, 4096], + [:rsa_key_5120, 5120], + [:rsa_key_8192, 8192], [:dsa_key_2048, 2048], [:ecdsa_key_256, 256], [:ed25519_key_256, 256] @@ -141,8 +158,11 @@ describe Gitlab::SSHPublicKey, lib: true do where(:factory, :fingerprint) do [ - [:rsa_key_2048, '2e:ca:dc:e0:37:29:ed:fc:f0:1d:bf:66:d4:cd:51:b1'], - [:dsa_key_2048, 'bc:c1:a4:be:7e:8c:84:56:b3:58:93:53:c6:80:78:8c'], + [:rsa_key_2048, '58:a8:9d:cd:1f:70:f8:5a:d9:e4:24:8e:da:89:e4:fc'], + [:rsa_key_4096, 'df:73:db:29:3c:a5:32:cf:09:17:7e:8e:9d:de:d7:f7'], + [:rsa_key_5120, 'fe:fa:3a:4d:7d:51:ec:bf:c7:64:0c:96:d0:17:8a:d0'], + [:rsa_key_8192, 'fb:53:7f:e9:2f:f7:17:aa:c8:32:52:06:8e:05:e2:82'], + [:dsa_key_2048, 'c8:85:1e:df:44:0f:20:00:3c:66:57:2b:21:10:5a:27'], [:ecdsa_key_256, '67:a3:a9:7d:b8:e1:15:d4:80:40:21:34:bb:ed:97:38'], [:ed25519_key_256, 'e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73'] ] diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index bf5703ac986..06d26ef89f1 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -12,6 +12,9 @@ describe Key, :mailer do it { is_expected.to validate_presence_of(:key) } it { is_expected.to validate_length_of(:key).is_at_most(5000) } it { is_expected.to allow_value(attributes_for(:rsa_key_2048)[:key]).for(:key) } + it { is_expected.to allow_value(attributes_for(:rsa_key_4096)[:key]).for(:key) } + it { is_expected.to allow_value(attributes_for(:rsa_key_5120)[:key]).for(:key) } + it { is_expected.to allow_value(attributes_for(:rsa_key_8192)[:key]).for(:key) } it { is_expected.to allow_value(attributes_for(:dsa_key_2048)[:key]).for(:key) } it { is_expected.to allow_value(attributes_for(:ecdsa_key_256)[:key]).for(:key) } it { is_expected.to allow_value(attributes_for(:ed25519_key_256)[:key]).for(:key) } @@ -103,24 +106,6 @@ describe Key, :mailer do end end - context 'validate size' do - where(:key_content, :result) do - [ - [Spec::Support::Helpers::KeyGeneratorHelper.new(512).generate, false], - [Spec::Support::Helpers::KeyGeneratorHelper.new(8192).generate, false], - [Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate, true] - ] - end - - with_them do - it 'validates the size of the key' do - key = build(:key, key: key_content) - - expect(key.valid?).to eq(result) - end - end - end - context 'validate it meets key restrictions' do where(:factory, :minimum, :result) do forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE From 5bb6124ce73379f708b10e7f4d6f98ed761ee3f6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 16 Feb 2018 17:20:28 +0000 Subject: [PATCH 018/161] Backport webpack.config.js changes from EE --- config/webpack.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 5ec33533b4f..f81f7067258 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -27,11 +27,11 @@ var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'ap // filter out entries currently imported dynamically in dispatcher.js var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString(); -var dispatcherChunks = dispatcher.match(/(?!import\('.\/)pages\/[^']+/g); +var dispatcherChunks = dispatcher.match(/(?!import\(')\.\/pages\/[^']+/g); pageEntries.forEach(( path ) => { let chunkPath = path.replace(/\/index\.js$/, ''); - if (!dispatcherChunks.includes(chunkPath)) { + if (!dispatcherChunks.includes('./' + chunkPath)) { let chunkName = chunkPath.replace(/\//g, '.'); autoEntries[chunkName] = './' + path; } From d54a80ca3a25612ffb85e47de769a7967999a232 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Fri, 16 Feb 2018 16:01:35 -0200 Subject: [PATCH 019/161] Update all articles with the new layout (meta data from the frontmatter) Context: https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/182 --- .../how_to_configure_ldap_gitlab_ce/index.md | 13 +++++---- .../examples/artifactory_and_gitlab/index.md | 10 +++---- .../laravel_with_gitlab_and_envoy/index.md | 10 +++---- doc/development/writing_documentation.md | 27 ++++++++++++------- doc/install/openshift_and_gitlab/index.md | 13 +++++---- doc/topics/git/how_to_install_git/index.md | 13 +++++---- .../index.md | 13 +++++---- .../pages/getting_started_part_four.md | 13 +++++---- .../project/pages/getting_started_part_one.md | 13 +++++---- .../pages/getting_started_part_three.md | 11 ++++---- .../project/pages/getting_started_part_two.md | 13 +++++---- 11 files changed, 88 insertions(+), 61 deletions(-) diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md index 9b9b8ca89e5..aa5e9513290 100644 --- a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md +++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md @@ -1,9 +1,12 @@ -# How to configure LDAP with GitLab CE +--- +author: Chris Wilson +author_gitlab: MrChrisW +level: intermediary +article_type: admin guide +date: 2017-05-03 +--- -> **[Article Type](../../../development/writing_documentation.html#types-of-technical-articles):** admin guide || -> **Level:** intermediary || -> **Author:** [Chris Wilson](https://gitlab.com/MrChrisW) || -> **Publication date:** 2017-05-03 +# How to configure LDAP with GitLab CE ## Introduction diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md index 8e91cd05d8a..d931c9a77f4 100644 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ b/doc/ci/examples/artifactory_and_gitlab/index.md @@ -1,14 +1,14 @@ --- redirect_from: 'https://docs.gitlab.com/ee/articles/artifactory_and_gitlab/index.html' +author: Fabio Busatto +author_gitlab: bikebilly +level: intermediary +article_type: tutorial +date: 2017-08-15 --- # How to deploy Maven projects to Artifactory with GitLab CI/CD -> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) || -> **Publication date:** 2017-08-15 - ## Introduction In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index e1aff6fdf36..b62874ef029 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -1,14 +1,14 @@ --- redirect_from: 'https://docs.gitlab.com/ee/articles/laravel_with_gitlab_and_envoy/index.html' +author: Mehran Rasulian +author_gitlab: mehranrasulian +level: intermediary +article_type: tutorial +date: 2017-08-31 --- # Test and deploy Laravel applications with GitLab CI/CD and Envoy -> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Mehran Rasulian](https://gitlab.com/mehranrasulian) || -> **Publication date:** 2017-08-31 - ## Introduction GitLab features our applications with Continuous Integration, and it is possible to easily deploy the new code changes to the production server whenever we want. diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 2a1d744668b..ceb0cdbb742 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -254,18 +254,25 @@ through the process of how to use it systematically. #### Special format -Every **Technical Article** contains, in the very beginning, a blockquote with the following information: +Every **Technical Article** contains a frontmatter at the beginning of the doc +with the following information: -- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial) -- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) -- A reference to the **author's name** and **GitLab.com handle** -- A reference of the **publication date** +- **Type of article** (user guide, admin guide, tech overview, tutorial) +- **Knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) +- **Author's name** and **GitLab.com handle** +- **Publication date** -```md -> **[Article Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Name Surname](https://gitlab.com/username) || -> **Publication date:** AAAA-MM-DD +For example: + + +```yaml +--- +author: John Doe +author_gitlab: johnDoe +level: beginner +article_type: user guide +date: 2017-02-01 +--- ``` #### Technical Articles - Writing Method diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md index 448cbe1077d..1f46ee4c1ea 100644 --- a/doc/install/openshift_and_gitlab/index.md +++ b/doc/install/openshift_and_gitlab/index.md @@ -1,9 +1,12 @@ -# Getting started with OpenShift Origin 3 and GitLab +--- +author: Achilleas Pipinellis +author_gitlab: axil +level: intermediary +article_type: tutorial +date: 2016-06-28 +--- -> **[Article Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) || -> **Publication date:** 2016-06-28 +# Getting started with OpenShift Origin 3 and GitLab ## Introduction diff --git a/doc/topics/git/how_to_install_git/index.md b/doc/topics/git/how_to_install_git/index.md index 7fb578e9ea8..6c909a1ba86 100644 --- a/doc/topics/git/how_to_install_git/index.md +++ b/doc/topics/git/how_to_install_git/index.md @@ -1,9 +1,12 @@ -# Installing Git +--- +author: Sean Packham +author_gitlab: SeanPackham +level: beginner +article_type: user guide +date: 2017-05-15 +--- -> **[Article Type](../../../development/writing_documentation.html#types-of-technical-articles):** user guide || -> **Level:** beginner || -> **Author:** [Sean Packham](https://gitlab.com/SeanPackham) || -> **Publication date:** 2017-05-15 +# Installing Git To begin contributing to GitLab projects you will need to install the Git client on your computer. diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md index 6a2f7b30dd3..4cb8f083fb5 100644 --- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md +++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md @@ -1,9 +1,12 @@ -# Numerous undo possibilities in Git +--- +author: Crt Mori +author_gitlab: Letme +level: intermediary +article_type: tutorial +date: 2017-05-15 +--- -> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Crt Mori](https://gitlab.com/Letme) || -> **Publication date:** 2017-08-17 +# Numerous undo possibilities in Git ## Introduction diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md index bd0cb437924..e338e1d8085 100644 --- a/doc/user/project/pages/getting_started_part_four.md +++ b/doc/user/project/pages/getting_started_part_four.md @@ -1,9 +1,12 @@ -# GitLab Pages from A to Z: Part 4 +--- +author: Marcia Ramos +author_gitlab: marcia +level: intermediate +article_type: user guide +date: 2017-02-22 +--- -> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide || -> **Level**: intermediate || -> **Author**: [Marcia Ramos](https://gitlab.com/marcia) || -> **Publication date:** 2017/02/22 +# GitLab Pages from A to Z: Part 4 - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 1e19f422d94..19f42890428 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -1,9 +1,12 @@ -# GitLab Pages from A to Z: Part 1 +--- +author: Marcia Ramos +author_gitlab: marcia +level: beginner +article_type: user guide +date: 2017-02-22 +--- -> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide || -> **Level**: beginner || -> **Author**: [Marcia Ramos](https://gitlab.com/marcia) || -> **Publication date:** 2017/02/22 +# GitLab Pages from A to Z: Part 1 - **Part 1: Static sites and GitLab Pages domains** - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index a153610c712..a3e12107c39 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -1,15 +1,14 @@ --- last_updated: 2017-09-28 +author: Marcia Ramos +author_gitlab: marcia +level: beginner +article_type: user guide +date: 2017-02-22 --- # GitLab Pages from A to Z: Part 3 -> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles)**: user guide || -> **Level**: beginner || -> **Author**: [Marcia Ramos](https://gitlab.com/marcia) || -> **Publication date:** 2017-02-22 || -> **Last updated**: 2017-09-28 - - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - **Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates** diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md index 4a724dd5c1b..06f706b83f5 100644 --- a/doc/user/project/pages/getting_started_part_two.md +++ b/doc/user/project/pages/getting_started_part_two.md @@ -1,9 +1,12 @@ -# GitLab Pages from A to Z: Part 2 +--- +author: Marcia Ramos +author_gitlab: marcia +level: beginner +article_type: user guide +date: 2017-02-22 +--- -> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide || -> **Level**: beginner || -> **Author**: [Marcia Ramos](https://gitlab.com/marcia) || -> **Publication date:** 2017/02/22 +# GitLab Pages from A to Z: Part 2 - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - **Part 2: Quick start guide - Setting up GitLab Pages** From d9b299aced0ec7ed983dd8e6ece86cef25d03e1a Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 15:35:01 -0600 Subject: [PATCH 020/161] migrate projects:milestones:index to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../javascripts/pages/projects/milestones/index/index.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index e4288dc1317..d7ab737246f 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'projects:milestones:index': - import('./pages/projects/milestones/index') - .then(callDefault) - .catch(fail); - break; case 'projects:milestones:show': import('./pages/projects/milestones/show') .then(callDefault) diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js index 8fb4d83d8a3..38789365a67 100644 --- a/app/assets/javascripts/pages/projects/milestones/index/index.js +++ b/app/assets/javascripts/pages/projects/milestones/index/index.js @@ -1,3 +1,3 @@ import milestones from '~/pages/milestones/shared'; -export default milestones; +document.addEventListener('DOMContentLoaded', milestones); From d163f906122d71a62e5c6bd2429a101a180863f6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:06:26 -0600 Subject: [PATCH 021/161] migrate projects:milestones:show to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../javascripts/pages/projects/milestones/show/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d7ab737246f..19a0a7ff1d5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'projects:milestones:show': - import('./pages/projects/milestones/show') - .then(callDefault) - .catch(fail); - break; case 'groups:milestones:show': import('./pages/groups/milestones/show') .then(callDefault) diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js index 35b5c9c2ced..84a52421598 100644 --- a/app/assets/javascripts/pages/projects/milestones/show/index.js +++ b/app/assets/javascripts/pages/projects/milestones/show/index.js @@ -1,7 +1,7 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; import milestones from '~/pages/milestones/shared'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { initMilestonesShow(); milestones(); -}; +}); From 5fa3bd178793d8084e495807b8b6cfa20974992b Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:09:34 -0600 Subject: [PATCH 022/161] migrate groups:milestones:show to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/javascripts/pages/groups/milestones/show/index.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 19a0a7ff1d5..216caceb9d5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'groups:milestones:show': - import('./pages/groups/milestones/show') - .then(callDefault) - .catch(fail); - break; case 'dashboard:milestones:show': import('./pages/dashboard/milestones/show') .then(callDefault) diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js index c9a18353f2e..88f40b5278e 100644 --- a/app/assets/javascripts/pages/groups/milestones/show/index.js +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -1,3 +1,3 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; -export default initMilestonesShow; +document.addEventListener('DOMContentLoaded', initMilestonesShow); From feb86d49b016d2bd422f28b89284bebbe4bedf19 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:11:14 -0600 Subject: [PATCH 023/161] migrate dashboard:milestones:show to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../javascripts/pages/dashboard/milestones/show/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 216caceb9d5..5297104e9a1 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'dashboard:milestones:show': - import('./pages/dashboard/milestones/show') - .then(callDefault) - .catch(fail); - break; case 'dashboard:issues': import('./pages/dashboard/issues') .then(callDefault) diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js index 2e7a08a369c..06195d73c0a 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -1,7 +1,7 @@ import Milestone from '~/milestone'; import Sidebar from '~/right_sidebar'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Milestone(); // eslint-disable-line no-new new Sidebar(); // eslint-disable-line no-new -}; +}); From 99bc2a4a2dbc3c53a030488d75136090b2c87aa1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:13:06 -0600 Subject: [PATCH 024/161] migrate dashboard:issues to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/javascripts/pages/dashboard/issues/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 5297104e9a1..e7acde79b80 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'dashboard:issues': - import('./pages/dashboard/issues') - .then(callDefault) - .catch(fail); - break; case 'dashboard:merge_requests': import('./pages/dashboard/merge_requests') .then(callDefault) diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js index b7353669e65..c4901dd1cb6 100644 --- a/app/assets/javascripts/pages/dashboard/issues/index.js +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -1,7 +1,7 @@ import projectSelect from '~/project_select'; import initLegacyFilters from '~/init_legacy_filters'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { projectSelect(); initLegacyFilters(); -}; +}); From 9bb3ff551c990db81246468314ef55a8a2a426b4 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:15:19 -0600 Subject: [PATCH 025/161] migrate dashboard:merge_requests to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../javascripts/pages/dashboard/merge_requests/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index e7acde79b80..c1716487fd3 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'dashboard:merge_requests': - import('./pages/dashboard/merge_requests') - .then(callDefault) - .catch(fail); - break; case 'groups:issues': import('./pages/groups/issues') .then(callDefault) diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index b7353669e65..c4901dd1cb6 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -1,7 +1,7 @@ import projectSelect from '~/project_select'; import initLegacyFilters from '~/init_legacy_filters'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { projectSelect(); initLegacyFilters(); -}; +}); From 17b8bfc8c6c4837b9f2fd825f8fcc192a6aaf962 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:15:57 -0600 Subject: [PATCH 026/161] migrate groups:issues to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/javascripts/pages/groups/issues/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index c1716487fd3..98142a1b9a6 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'groups:issues': - import('./pages/groups/issues') - .then(callDefault) - .catch(fail); - break; case 'groups:merge_requests': import('./pages/groups/merge_requests') .then(callDefault) diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index fbdfabd1e95..d149b307e7f 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -2,9 +2,9 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, }); projectSelect(); -}; +}); From 8878dce8ce1a8a0550a5df9ee8d5f49be28abbd0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:17:00 -0600 Subject: [PATCH 027/161] migrate groups:merge_requests to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/javascripts/pages/groups/merge_requests/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 98142a1b9a6..7ce8856e77e 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'groups:merge_requests': - import('./pages/groups/merge_requests') - .then(callDefault) - .catch(fail); - break; case 'dashboard:todos:index': import('./pages/dashboard/todos/index') .then(callDefault) diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index f6d284bf9ef..a5cc1f34b63 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -2,9 +2,9 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, }); projectSelect(); -}; +}); From 96feb536e426e9f1f54fa14f50cab9f2a78754be Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:19:07 -0600 Subject: [PATCH 028/161] migrate dashboard:todos:index to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/javascripts/pages/dashboard/todos/index/index.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 7ce8856e77e..bd082bba09f 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'dashboard:todos:index': - import('./pages/dashboard/todos/index') - .then(callDefault) - .catch(fail); - break; case 'admin:jobs:index': import('./pages/admin/jobs/index') .then(callDefault) diff --git a/app/assets/javascripts/pages/dashboard/todos/index/index.js b/app/assets/javascripts/pages/dashboard/todos/index/index.js index 77c23685943..9d2c2f2994f 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/index.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/index.js @@ -1,3 +1,3 @@ import Todos from './todos'; -export default () => new Todos(); +document.addEventListener('DOMContentLoaded', () => new Todos()); From e95d4822960c6541bb429479853b8232aa669335 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:22:26 -0600 Subject: [PATCH 029/161] migrate admin:jobs:index to static bundle --- app/assets/javascripts/pages/admin/jobs/index/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js index 31d58eabaaf..5a4f8c6e745 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/index.js +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -1,12 +1,10 @@ import Vue from 'vue'; - import Translate from '~/vue_shared/translate'; - import stopJobsModal from './components/stop_jobs_modal.vue'; Vue.use(Translate); -export default () => { +document.addEventListener('DOMContentLoaded', () => { const stopJobsButton = document.getElementById('stop-jobs-button'); if (stopJobsButton) { // eslint-disable-next-line no-new @@ -27,4 +25,4 @@ export default () => { }, }); } -}; +}); From c6d6623040e8be6c70e8d4166939143b21217658 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:27:21 -0600 Subject: [PATCH 030/161] migrate dashboard:projects:* to static bundle --- app/assets/javascripts/dispatcher.js | 46 ++++++++----------- .../pages/dashboard/projects/index.js | 2 +- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index bd082bba09f..8fac9cc0769 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,32 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'admin:jobs:index': - import('./pages/admin/jobs/index') - .then(callDefault) - .catch(fail); - break; - case 'admin:projects:index': - import('./pages/admin/projects/index/index') - .then(callDefault) - .catch(fail); - break; - case 'admin:users:index': - import('./pages/admin/users/shared') - .then(callDefault) - .catch(fail); - break; - case 'admin:users:show': - import('./pages/admin/users/shared') - .then(callDefault) - .catch(fail); - break; - case 'dashboard:projects:index': - case 'dashboard:projects:starred': - import('./pages/dashboard/projects') - .then(callDefault) - .catch(fail); - break; case 'explore:projects:index': case 'explore:projects:trending': case 'explore:projects:starred': @@ -469,6 +443,26 @@ var Dispatcher; .then(callDefault) .catch(fail); break; + case 'admin:jobs:index': + import('./pages/admin/jobs/index') + .then(callDefault) + .catch(fail); + break; + case 'admin:projects:index': + import('./pages/admin/projects/index/index') + .then(callDefault) + .catch(fail); + break; + case 'admin:users:index': + import('./pages/admin/users/shared') + .then(callDefault) + .catch(fail); + break; + case 'admin:users:show': + import('./pages/admin/users/shared') + .then(callDefault) + .catch(fail); + break; } switch (path[0]) { case 'sessions': diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js index c88cbf1a6ba..0c585e162cb 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index.js +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -1,3 +1,3 @@ import ProjectsList from '~/projects_list'; -export default () => new ProjectsList(); +document.addEventListener('DOMContentLoaded', () => new ProjectsList()); From 754dc137f70709eb3d7e6853ffa83f3017e93dbe Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:30:28 -0600 Subject: [PATCH 031/161] migrate explore:projects:* to static bundle --- app/assets/javascripts/dispatcher.js | 7 ------- app/assets/javascripts/pages/explore/projects/index.js | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 8fac9cc0769..4b4906bd6ee 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,13 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'explore:projects:index': - case 'explore:projects:trending': - case 'explore:projects:starred': - import('./pages/explore/projects') - .then(callDefault) - .catch(fail); - break; case 'explore:groups:index': import('./pages/explore/groups') .then(callDefault) diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js index c88cbf1a6ba..0c585e162cb 100644 --- a/app/assets/javascripts/pages/explore/projects/index.js +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -1,3 +1,3 @@ import ProjectsList from '~/projects_list'; -export default () => new ProjectsList(); +document.addEventListener('DOMContentLoaded', () => new ProjectsList()); From 6e0f420ce95e94b1a2cd509577bf6953a48f9b6f Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:34:35 -0600 Subject: [PATCH 032/161] migrate explore:groups:index to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/javascripts/pages/explore/groups/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 4b4906bd6ee..aae499fe688 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,11 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'explore:groups:index': - import('./pages/explore/groups') - .then(callDefault) - .catch(fail); - break; case 'projects:milestones:new': case 'projects:milestones:create': import('./pages/projects/milestones/new') diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js index e59c38b8bc4..3c7edbdd7c7 100644 --- a/app/assets/javascripts/pages/explore/groups/index.js +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -2,7 +2,7 @@ import GroupsList from '~/groups_list'; import Landing from '~/landing'; import initGroupsList from '../../../groups'; -export default function () { +document.addEventListener('DOMContentLoaded', () => { new GroupsList(); // eslint-disable-line no-new initGroupsList(); const landingElement = document.querySelector('.js-explore-groups-landing'); @@ -13,4 +13,4 @@ export default function () { 'explore_groups_landing_dismissed', ); exploreGroupsLanding.toggle(); -} +}); From bd36fe31327ea5f075e6b1896d7a634afde61fa6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:41:04 -0600 Subject: [PATCH 033/161] migrate projects:milestones:* and groups:milestones:* to static bundle --- app/assets/javascripts/dispatcher.js | 24 ------------------- .../pages/groups/milestones/edit/index.js | 2 +- .../pages/groups/milestones/new/index.js | 2 +- .../pages/projects/milestones/edit/index.js | 2 +- .../pages/projects/milestones/new/index.js | 2 +- 5 files changed, 4 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index aae499fe688..156f564d0d0 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -53,30 +53,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'projects:milestones:new': - case 'projects:milestones:create': - import('./pages/projects/milestones/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:milestones:edit': - case 'projects:milestones:update': - import('./pages/projects/milestones/edit') - .then(callDefault) - .catch(fail); - break; - case 'groups:milestones:new': - case 'groups:milestones:create': - import('./pages/groups/milestones/new') - .then(callDefault) - .catch(fail); - break; - case 'groups:milestones:edit': - case 'groups:milestones:update': - import('./pages/groups/milestones/edit') - .then(callDefault) - .catch(fail); - break; case 'projects:compare:show': import('./pages/projects/compare/show') .then(callDefault) diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js index 5c99c90e24d..ddd10fe5062 100644 --- a/app/assets/javascripts/pages/groups/milestones/edit/index.js +++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(false); +document.addEventListener('DOMContentLoaded', () => initForm(false)); diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js index 5c99c90e24d..ddd10fe5062 100644 --- a/app/assets/javascripts/pages/groups/milestones/new/index.js +++ b/app/assets/javascripts/pages/groups/milestones/new/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(false); +document.addEventListener('DOMContentLoaded', () => initForm(false)); diff --git a/app/assets/javascripts/pages/projects/milestones/edit/index.js b/app/assets/javascripts/pages/projects/milestones/edit/index.js index 10e3979a36e..9a4ebf9890d 100644 --- a/app/assets/javascripts/pages/projects/milestones/edit/index.js +++ b/app/assets/javascripts/pages/projects/milestones/edit/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(); +document.addEventListener('DOMContentLoaded', () => initForm()); diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js index 10e3979a36e..9a4ebf9890d 100644 --- a/app/assets/javascripts/pages/projects/milestones/new/index.js +++ b/app/assets/javascripts/pages/projects/milestones/new/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -export default () => initForm(); +document.addEventListener('DOMContentLoaded', () => initForm()); From 92168708a9a2ec44206148586df81ddccc1b2bb6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:42:55 -0600 Subject: [PATCH 034/161] migrate projects:environments:metrics to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../javascripts/pages/projects/environments/metrics/index.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 156f564d0d0..3084042fda8 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -43,11 +43,6 @@ var Dispatcher; }); switch (page) { - case 'projects:environments:metrics': - import('./pages/projects/environments/metrics') - .then(callDefault) - .catch(fail); - break; case 'projects:merge_requests:index': case 'projects:issues:index': case 'projects:issues:show': diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js index f4760cb2720..0b644780ad4 100644 --- a/app/assets/javascripts/pages/projects/environments/metrics/index.js +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -1,3 +1,3 @@ import monitoringBundle from '~/monitoring/monitoring_bundle'; -export default monitoringBundle; +document.addEventListener('DOMContentLoaded', monitoringBundle); From bba4126311257628bb136de6b7a1d83a262bbd8e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:46:54 -0600 Subject: [PATCH 035/161] migrate projects:compare:show to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/javascripts/pages/projects/compare/show/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 3084042fda8..6cc71e03855 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -48,11 +48,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'projects:compare:show': - import('./pages/projects/compare/show') - .then(callDefault) - .catch(fail); - break; case 'projects:branches:new': import('./pages/projects/branches/new') .then(callDefault) diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js index 6b8d4503568..2b4fd3c47c0 100644 --- a/app/assets/javascripts/pages/projects/compare/show/index.js +++ b/app/assets/javascripts/pages/projects/compare/show/index.js @@ -1,8 +1,8 @@ import Diff from '~/diff'; import initChangesDropdown from '~/init_changes_dropdown'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Diff(); // eslint-disable-line no-new const paddingTop = 16; initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); -}; +}); From 8c92330c5074ad2cc723a9bbd56d80d44b52afd9 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:48:17 -0600 Subject: [PATCH 036/161] migrate projects:branches:new to static bundle --- app/assets/javascripts/dispatcher.js | 10 ---------- .../javascripts/pages/projects/branches/new/index.js | 4 +++- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 6cc71e03855..1566084f2d8 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -48,16 +48,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'projects:branches:new': - import('./pages/projects/branches/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:branches:create': - import('./pages/projects/branches/new') - .then(callDefault) - .catch(fail); - break; case 'projects:branches:index': import('./pages/projects/branches/index') .then(callDefault) diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js index ae5e033e97e..d32d5c6cb29 100644 --- a/app/assets/javascripts/pages/projects/branches/new/index.js +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -1,3 +1,5 @@ import NewBranchForm from '~/new_branch_form'; -export default () => new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); +document.addEventListener('DOMContentLoaded', () => ( + new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)) +)); From be6bd83b4cb001ebe59c2b62f59faacc585e4fc7 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:50:24 -0600 Subject: [PATCH 037/161] migrate projects:branches:index to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../javascripts/pages/projects/branches/index/index.js | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 1566084f2d8..c239fbd1a43 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -48,11 +48,6 @@ var Dispatcher; case 'projects:issues:show': shortcut_handler = true; break; - case 'projects:branches:index': - import('./pages/projects/branches/index') - .then(callDefault) - .catch(fail); - break; case 'projects:issues:new': import('./pages/projects/issues/new') .then(callDefault) diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index cee0f19bf2a..8fa266a37ce 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -1,7 +1,7 @@ import AjaxLoadingSpinner from '~/ajax_loading_spinner'; import DeleteModal from '~/branches/branches_delete_modal'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { AjaxLoadingSpinner.init(); new DeleteModal(); // eslint-disable-line no-new -}; +}); From a6af8e8e643a2779f54c0a07b1d79d86221aacdc Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:55:18 -0600 Subject: [PATCH 038/161] migrate projects:issues:new and projects:issues:edit to static bundle --- app/assets/javascripts/dispatcher.js | 10 ---------- .../javascripts/pages/projects/issues/edit/index.js | 4 +--- .../javascripts/pages/projects/issues/new/index.js | 4 +--- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index c239fbd1a43..4c79c3c4948 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -46,18 +46,8 @@ var Dispatcher; case 'projects:merge_requests:index': case 'projects:issues:index': case 'projects:issues:show': - shortcut_handler = true; - break; case 'projects:issues:new': - import('./pages/projects/issues/new') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; case 'projects:issues:edit': - import('./pages/projects/issues/edit') - .then(callDefault) - .catch(fail); shortcut_handler = true; break; case 'projects:merge_requests:creations:new': diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js index 7f27f379d8c..ffc84dc106b 100644 --- a/app/assets/javascripts/pages/projects/issues/edit/index.js +++ b/app/assets/javascripts/pages/projects/issues/edit/index.js @@ -1,5 +1,3 @@ import initForm from '../form'; -export default () => { - initForm(); -}; +document.addEventListener('DOMContentLoaded', initForm); diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js index 7f27f379d8c..ffc84dc106b 100644 --- a/app/assets/javascripts/pages/projects/issues/new/index.js +++ b/app/assets/javascripts/pages/projects/issues/new/index.js @@ -1,5 +1,3 @@ import initForm from '../form'; -export default () => { - initForm(); -}; +document.addEventListener('DOMContentLoaded', initForm); From dc65609e726b78098e82becdd6224fc87a37130d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:57:10 -0600 Subject: [PATCH 039/161] migrate projects:merge_requests:creations:new to static bundle --- app/assets/javascripts/dispatcher.js | 4 ---- .../pages/projects/merge_requests/creations/new/index.js | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 4c79c3c4948..2e57a2d2c22 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -50,10 +50,6 @@ var Dispatcher; case 'projects:issues:edit': shortcut_handler = true; break; - case 'projects:merge_requests:creations:new': - import('./pages/projects/merge_requests/creations/new') - .then(callDefault) - .catch(fail); case 'projects:merge_requests:creations:diffs': import('./pages/projects/merge_requests/creations/diffs') .then(callDefault) diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index ccd0b54c5ed..1d5aec4001d 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -1,7 +1,7 @@ import Compare from '~/compare'; import MergeRequest from '~/merge_request'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); if (mrNewCompareNode) { new Compare({ // eslint-disable-line no-new @@ -15,4 +15,4 @@ export default () => { action: mrNewSubmitNode.dataset.mrSubmitAction, }); } -}; +}); From a9f320b5a2acd092c7b4523ff8ab7160c337db20 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:59:36 -0600 Subject: [PATCH 040/161] migrate projects:merge_requests:creations:diffs to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../pages/projects/merge_requests/creations/diffs/index.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 2e57a2d2c22..04c5d220df2 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -48,12 +48,7 @@ var Dispatcher; case 'projects:issues:show': case 'projects:issues:new': case 'projects:issues:edit': - shortcut_handler = true; - break; case 'projects:merge_requests:creations:diffs': - import('./pages/projects/merge_requests/creations/diffs') - .then(callDefault) - .catch(fail); shortcut_handler = true; break; case 'projects:merge_requests:edit': diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js index 734d01ae6f2..febfecebbd2 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js @@ -1,3 +1,3 @@ import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; -export default initMergeRequest; +document.addEventListener('DOMContentLoaded', initMergeRequest); From 71ac339fcaee5611506ea250bf501ecb49d202b0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 17:06:10 -0600 Subject: [PATCH 041/161] migrate projects:merge_requests:edit to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- .../javascripts/pages/projects/merge_requests/edit/index.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 04c5d220df2..089ffa77677 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -49,12 +49,7 @@ var Dispatcher; case 'projects:issues:new': case 'projects:issues:edit': case 'projects:merge_requests:creations:diffs': - shortcut_handler = true; - break; case 'projects:merge_requests:edit': - import('./pages/projects/merge_requests/edit') - .then(callDefault) - .catch(fail); shortcut_handler = true; break; case 'projects:tags:new': diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js index 734d01ae6f2..febfecebbd2 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js @@ -1,3 +1,3 @@ import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; -export default initMergeRequest; +document.addEventListener('DOMContentLoaded', initMergeRequest); From fab0f24c67833a6640830febdd1b705395a13484 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 18:14:24 -0600 Subject: [PATCH 042/161] alias create and update actions to new and edit --- app/helpers/webpack_helper.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 9d071f2d59a..8bcced70d63 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -7,17 +7,24 @@ module WebpackHelper def webpack_controller_bundle_tags bundles = [] - segments = [*controller.controller_path.split('/'), controller.action_name].compact - until segments.empty? + action = case controller.action_name + when 'create' then 'new' + when 'update' then 'edit' + else controller.action_name + end + + route = [*controller.controller_path.split('/'), action].compact + + until route.empty? begin - asset_paths = gitlab_webpack_asset_paths("pages.#{segments.join('.')}", extension: 'js') + asset_paths = gitlab_webpack_asset_paths("pages.#{route.join('.')}", extension: 'js') bundles.unshift(*asset_paths) rescue Webpack::Rails::Manifest::EntryPointMissingError # no bundle exists for this path end - segments.pop + route.pop end javascript_include_tag(*bundles) From eef63813ea6a9585941be5f079104604d779d440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mica=C3=ABl=20Bergeron?= Date: Fri, 9 Feb 2018 10:32:42 -0500 Subject: [PATCH 043/161] stop ProcessCommitWorker from processing MR merge commit When a merge request is merged, it creates a commit with the description of the MR, which may contain references and issue closing references. As this will be handled in the PostMergeService anyways, let's ignore merge commit generated from a MR. --- app/models/commit.rb | 8 ++--- app/workers/process_commit_worker.rb | 12 +++---- spec/workers/process_commit_worker_spec.rb | 38 ++++++++++++++++++---- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 8c960389652..add5fcf0e79 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -417,6 +417,10 @@ class Commit !!(title =~ WIP_REGEX) end + def merged_merge_request?(user) + !!merged_merge_request(user) + end + private def commit_reference(from, referable_commit_id, full: false) @@ -445,10 +449,6 @@ class Commit changes end - def merged_merge_request?(user) - !!merged_merge_request(user) - end - def merged_merge_request_no_cache(user) MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 52eebe475ec..a6330a6c51f 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -23,27 +23,25 @@ class ProcessCommitWorker return unless user commit = build_commit(project, commit_hash) - author = commit.author || user - process_commit_message(project, commit, user, author, default) + # this is a GitLab generated commit message, ignore it. + return if commit.merged_merge_request?(user) + process_commit_message(project, commit, user, author, default) update_issue_metrics(commit, author) end def process_commit_message(project, commit, user, author, default = false) closed_issues = default ? commit.closes_issues(user) : [] - unless closed_issues.empty? - close_issues(project, user, author, commit, closed_issues) - end - + close_issues(project, user, author, commit, closed_issues) if closed_issues.any? commit.create_cross_references!(author, closed_issues) end def close_issues(project, user, author, commit, issues) # We don't want to run permission related queries for every single issue, - # therefor we use IssueCollection here and skip the authorization check in + # therefore we use IssueCollection here and skip the authorization check in # Issues::CloseService#execute. IssueCollection.new(issues).updatable_by_user(user).each do |issue| Issues::CloseService.new(project, author) diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 24f8ca67594..cd6918470a0 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -20,6 +20,32 @@ describe ProcessCommitWorker do worker.perform(project.id, -1, commit.to_hash) end + context 'when commit is a merge request merge commit' do + let(:merge_request) do + create(:merge_request, + description: "Closes #{issue.to_reference}", + source_branch: 'feature-merged', + target_branch: 'master', + source_project: project) + end + + let(:commit) do + project.repository.create_branch('feature-merged', 'feature') + + sha = project.repository.merge(user, + merge_request.diff_head_sha, + merge_request, + "Closes #{issue.to_reference}") + project.repository.commit(sha) + end + + it 'does not process the commit' do + expect(worker).not_to receive(:close_issues) + + worker.perform(project.id, user.id, commit.to_hash) + end + end + it 'processes the commit message' do expect(worker).to receive(:process_commit_message).and_call_original @@ -49,10 +75,10 @@ describe ProcessCommitWorker do context 'when pushing to the default branch' do it 'closes issues that should be closed per the commit message' do allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + .and_return("Closes #{issue.to_reference}") expect(worker).to receive(:close_issues) - .with(project, user, user, commit, [issue]) + .with(project, user, user, commit, [issue]) worker.process_commit_message(project, commit, user, user, true) end @@ -61,7 +87,7 @@ describe ProcessCommitWorker do context 'when pushing to a non-default branch' do it 'does not close any issues' do allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + .and_return("Closes #{issue.to_reference}") expect(worker).not_to receive(:close_issues) @@ -103,7 +129,7 @@ describe ProcessCommitWorker do describe '#update_issue_metrics' do it 'updates any existing issue metrics' do allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + .and_return("Closes #{issue.to_reference}") worker.update_issue_metrics(commit, user) @@ -114,7 +140,7 @@ describe ProcessCommitWorker do it "doesn't execute any queries with false conditions" do allow(commit).to receive(:safe_message) - .and_return("Lorem Ipsum") + .and_return("Lorem Ipsum") expect { worker.update_issue_metrics(commit, user) }.not_to make_queries_matching(/WHERE (?:1=0|0=1)/) end @@ -129,7 +155,7 @@ describe ProcessCommitWorker do it 'parses date strings into Time instances' do commit = worker - .build_commit(project, id: '123', authored_date: Time.now.to_s) + .build_commit(project, id: '123', authored_date: Time.now.to_s) expect(commit.authored_date).to be_an_instance_of(Time) end From 1883c5964085ab18a12633ae5db663298f176ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mica=C3=ABl=20Bergeron?= Date: Fri, 9 Feb 2018 10:44:28 -0500 Subject: [PATCH 044/161] add changelog --- .../unreleased/32564-fix-double-system-closing-notes.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/32564-fix-double-system-closing-notes.yml diff --git a/changelogs/unreleased/32564-fix-double-system-closing-notes.yml b/changelogs/unreleased/32564-fix-double-system-closing-notes.yml new file mode 100644 index 00000000000..e6e1ef8c76d --- /dev/null +++ b/changelogs/unreleased/32564-fix-double-system-closing-notes.yml @@ -0,0 +1,5 @@ +--- +title: Fix duplicate system notes when merging a merge request. +merge_request: 17035 +author: +type: fixed From f9492554617d807b35358cb799d26e3422dd4c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mica=C3=ABl=20Bergeron?= Date: Mon, 12 Feb 2018 09:02:15 -0500 Subject: [PATCH 045/161] fix specs --- lib/gitlab/git/commit.rb | 8 ++++++++ spec/workers/process_commit_worker_spec.rb | 23 ++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index d95561fe1b2..28a8a79e36e 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -471,6 +471,14 @@ module Gitlab private + def parent_ids=(shas) + @parent_ids = case shas + when String then JSON.parse(shas) + else + shas + end + end + def init_from_hash(hash) raw_commit = hash.symbolize_keys diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index cd6918470a0..ca560fa63e2 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -74,11 +74,9 @@ describe ProcessCommitWorker do describe '#process_commit_message' do context 'when pushing to the default branch' do it 'closes issues that should be closed per the commit message' do - allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") - expect(worker).to receive(:close_issues) - .with(project, user, user, commit, [issue]) + expect(worker).to receive(:close_issues).with(project, user, user, commit, [issue]) worker.process_commit_message(project, commit, user, user, true) end @@ -86,8 +84,7 @@ describe ProcessCommitWorker do context 'when pushing to a non-default branch' do it 'does not close any issues' do - allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") expect(worker).not_to receive(:close_issues) @@ -128,8 +125,7 @@ describe ProcessCommitWorker do describe '#update_issue_metrics' do it 'updates any existing issue metrics' do - allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") worker.update_issue_metrics(commit, user) @@ -139,10 +135,10 @@ describe ProcessCommitWorker do end it "doesn't execute any queries with false conditions" do - allow(commit).to receive(:safe_message) - .and_return("Lorem Ipsum") + allow(commit).to receive(:safe_message).and_return("Lorem Ipsum") - expect { worker.update_issue_metrics(commit, user) }.not_to make_queries_matching(/WHERE (?:1=0|0=1)/) + expect { worker.update_issue_metrics(commit, user) } + .not_to make_queries_matching(/WHERE (?:1=0|0=1)/) end end @@ -154,8 +150,9 @@ describe ProcessCommitWorker do end it 'parses date strings into Time instances' do - commit = worker - .build_commit(project, id: '123', authored_date: Time.now.to_s) + commit = worker.build_commit(project, + id: '123', + authored_date: Time.now.to_s) expect(commit.authored_date).to be_an_instance_of(Time) end From af5cd10e00402937310dfe8e4dfa48b0c15b157a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mica=C3=ABl=20Bergeron?= Date: Fri, 16 Feb 2018 13:49:07 -0500 Subject: [PATCH 046/161] applying feedback # modified: lib/gitlab/git/commit.rb --- app/workers/process_commit_worker.rb | 6 +++--- lib/gitlab/git/commit.rb | 3 ++- spec/workers/process_commit_worker_spec.rb | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index a6330a6c51f..5b25d980bdb 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -25,14 +25,14 @@ class ProcessCommitWorker commit = build_commit(project, commit_hash) author = commit.author || user - # this is a GitLab generated commit message, ignore it. - return if commit.merged_merge_request?(user) - process_commit_message(project, commit, user, author, default) update_issue_metrics(commit, author) end def process_commit_message(project, commit, user, author, default = false) + # this is a GitLab generated commit message, ignore it. + return if commit.merged_merge_request?(user) + closed_issues = default ? commit.closes_issues(user) : [] close_issues(project, user, author, commit, closed_issues) if closed_issues.any? diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 28a8a79e36e..ea59978c58a 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -473,7 +473,8 @@ module Gitlab def parent_ids=(shas) @parent_ids = case shas - when String then JSON.parse(shas) + when String + JSON.parse(shas) else shas end diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index ca560fa63e2..76ef57b6b1e 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -39,7 +39,7 @@ describe ProcessCommitWorker do project.repository.commit(sha) end - it 'does not process the commit' do + it 'it does not close any issues from the commit message' do expect(worker).not_to receive(:close_issues) worker.perform(project.id, user.id, commit.to_hash) From dc9b2a8fb2a282aefa946246b0fbaf15562fb4a5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 13:01:07 -0600 Subject: [PATCH 047/161] correct for missing break statement in dispatcher.js --- app/assets/javascripts/dispatcher.js | 1 + .../pages/projects/merge_requests/creations/{diffs => }/index.js | 0 2 files changed, 1 insertion(+) rename app/assets/javascripts/pages/projects/merge_requests/creations/{diffs => }/index.js (100%) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 089ffa77677..b6754689466 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -48,6 +48,7 @@ var Dispatcher; case 'projects:issues:show': case 'projects:issues:new': case 'projects:issues:edit': + case 'projects:merge_requests:creations:new': case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:edit': shortcut_handler = true; diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js similarity index 100% rename from app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js rename to app/assets/javascripts/pages/projects/merge_requests/creations/index.js From b63674e2c677bdbfc40b89d06af86771a807f6d1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 13:08:05 -0600 Subject: [PATCH 048/161] migrate admin:users:* to static bundle --- app/assets/javascripts/dispatcher.js | 20 ------------------- .../pages/admin/projects/index/index.js | 4 ++-- .../components/delete_user_modal.vue | 0 .../pages/admin/users/{shared => }/index.js | 4 ++-- 4 files changed, 4 insertions(+), 24 deletions(-) rename app/assets/javascripts/pages/admin/users/{shared => }/components/delete_user_modal.vue (100%) rename app/assets/javascripts/pages/admin/users/{shared => }/index.js (95%) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index b6754689466..8467375f81a 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -359,26 +359,6 @@ var Dispatcher; .then(callDefault) .catch(fail); break; - case 'admin:jobs:index': - import('./pages/admin/jobs/index') - .then(callDefault) - .catch(fail); - break; - case 'admin:projects:index': - import('./pages/admin/projects/index/index') - .then(callDefault) - .catch(fail); - break; - case 'admin:users:index': - import('./pages/admin/users/shared') - .then(callDefault) - .catch(fail); - break; - case 'admin:users:show': - import('./pages/admin/users/shared') - .then(callDefault) - .catch(fail); - break; } switch (path[0]) { case 'sessions': diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index a87b27090a8..3c597a1093e 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -5,7 +5,7 @@ import csrf from '~/lib/utils/csrf'; import deleteProjectModal from './components/delete_project_modal.vue'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { Vue.use(Translate); const deleteProjectModalEl = document.getElementById('delete-project-modal'); @@ -34,4 +34,4 @@ export default () => { deleteModal.projectName = buttonProps.projectName; } }); -}; +}); diff --git a/app/assets/javascripts/pages/admin/users/shared/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue similarity index 100% rename from app/assets/javascripts/pages/admin/users/shared/components/delete_user_modal.vue rename to app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue diff --git a/app/assets/javascripts/pages/admin/users/shared/index.js b/app/assets/javascripts/pages/admin/users/index.js similarity index 95% rename from app/assets/javascripts/pages/admin/users/shared/index.js rename to app/assets/javascripts/pages/admin/users/index.js index d2a0f82fa2b..4f5d6b55031 100644 --- a/app/assets/javascripts/pages/admin/users/shared/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -5,7 +5,7 @@ import csrf from '~/lib/utils/csrf'; import deleteUserModal from './components/delete_user_modal.vue'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { Vue.use(Translate); const deleteUserModalEl = document.getElementById('delete-user-modal'); @@ -40,4 +40,4 @@ export default () => { deleteModal.username = buttonProps.username; } }); -}; +}); From 93de4ebd9efe49d9cbf7678d77b2a1574f5b4e91 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Feb 2018 13:59:22 -0600 Subject: [PATCH 049/161] Re-add common_vue --- app/views/projects/merge_requests/show.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index b0fa6c2bd7b..e29f21b3bec 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -4,6 +4,8 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes +- content_for :page_specific_javascripts do + = webpack_bundle_tag('common_vue') .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } } = render "projects/merge_requests/mr_title" From e8d4708be9e050e7eb9aaa5db63a37f965146e57 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Feb 2018 14:28:54 -0600 Subject: [PATCH 050/161] Remove export from pages --- app/assets/javascripts/pages/projects/jobs/show/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js index cecbfb82946..3626f3ffec6 100644 --- a/app/assets/javascripts/pages/projects/jobs/show/index.js +++ b/app/assets/javascripts/pages/projects/jobs/show/index.js @@ -1,3 +1,3 @@ import initJobDetails from '~/jobs/job_details_bundle'; -export default initJobDetails; +document.addEventListener('DOMContentLoaded', initJobDetails); From c7c211c28bc9e2594bbfa2b8f737b35b32e34934 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Feb 2018 14:33:01 -0600 Subject: [PATCH 051/161] Wrap pages in DOMContentLoaded --- .../javascripts/pages/projects/merge_requests/show/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 8552e49a9cf..07f3e579c97 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -8,7 +8,7 @@ import Diff from '~/diff'; import { handleLocationHash } from '~/lib/utils/common_utils'; import howToMerge from '~/how_to_merge'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Diff(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new @@ -25,4 +25,4 @@ export default () => { new ShortcutsIssuable(true); // eslint-disable-line no-new handleLocationHash(); howToMerge(); -}; +}); From 17d842db303759b48c8ed6235f462d98d68d1bca Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 16 Feb 2018 20:07:24 +0000 Subject: [PATCH 052/161] Fix order dependencies in some specs Our automatically-generated project paths are of the form project. If a spec manually specifies a project path of that form, it may conflict with the automatically-generated paths in some circumstances. --- spec/requests/api/issues_spec.rb | 2 +- spec/requests/api/projects_spec.rb | 4 ++-- spec/requests/api/v3/issues_spec.rb | 2 +- spec/requests/api/v3/projects_spec.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 13db40d21a5..13bb3b92085 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1380,7 +1380,7 @@ describe API::Issues, :mailer do end describe '/projects/:id/issues/:issue_iid/move' do - let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } it 'moves an issue' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 00dd8897e6a..cee93f6ed14 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -7,7 +7,7 @@ describe API::Projects do let(:user3) { create(:user) } let(:admin) { create(:admin) } let(:project) { create(:project, namespace: user.namespace) } - let(:project2) { create(:project, path: 'project2', namespace: user.namespace) } + let(:project2) { create(:project, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } @@ -315,7 +315,7 @@ describe API::Projects do context 'and with all query parameters' do let!(:project5) { create(:project, :public, path: 'gitlab5', namespace: create(:namespace)) } - let!(:project6) { create(:project, :public, path: 'project6', namespace: user.namespace) } + let!(:project6) { create(:project, :public, namespace: user.namespace) } let!(:project7) { create(:project, :public, path: 'gitlab7', namespace: user.namespace) } let!(:project8) { create(:project, path: 'gitlab8', namespace: user.namespace) } let!(:project9) { create(:project, :public, path: 'gitlab9') } diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 0dd6d673625..25b4c8438e4 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1191,7 +1191,7 @@ describe API::V3::Issues, :mailer do end describe '/projects/:id/issues/:issue_id/move' do - let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } it 'moves an issue' do diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index bf36d3e245a..4c25bd935c6 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -6,7 +6,7 @@ describe API::V3::Projects do let(:user3) { create(:user) } let(:admin) { create(:admin) } let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } + let(:project2) { create(:project, creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } From a978a5e01de26ad8d96312cb0a07087ec2dccb21 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Feb 2018 22:19:22 +0000 Subject: [PATCH 053/161] Revert "Merge branch 'expired-ci-artifacts' into 'master'" This reverts merge request !16578 --- app/models/ci/build.rb | 37 ++----------------- .../unreleased/expired-ci-artifacts.yml | 5 --- ...0180119160751_optimize_ci_job_artifacts.rb | 23 ------------ db/schema.rb | 2 - spec/factories/ci/builds.rb | 4 +- 5 files changed, 6 insertions(+), 65 deletions(-) delete mode 100644 changelogs/unreleased/expired-ci-artifacts.yml delete mode 100644 db/migrate/20180119160751_optimize_ci_job_artifacts.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 490edf4ac57..ee987949080 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -41,41 +41,12 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } - - # This convoluted mess is because we need to handle two cases of - # artifact files during the migration. And a simple OR clause - # makes it impossible to optimize. - - # Instead we want to use UNION ALL and do two carefully - # constructed disjoint queries. But Rails cannot handle UNION or - # UNION ALL queries so we do the query in a subquery and wrap it - # in an otherwise redundant WHERE IN query (IN is fine for - # non-null columns). - - # This should all be ripped out when the migration is finished and - # replaced with just the new storage to avoid the extra work. - scope :with_artifacts, ->() do - old = Ci::Build.select(:id).where(%q[artifacts_file <> '']) - new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)], - Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id')) - where('ci_builds.id IN (? UNION ALL ?)', old, new) + where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', + '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id')) end - - scope :with_artifacts_not_expired, ->() do - old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND (artifacts_expire_at IS NULL OR artifacts_expire_at > ?)], Time.now) - new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)], - Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND (expire_at IS NULL OR expire_at > ?)', Time.now)) - where('ci_builds.id IN (? UNION ALL ?)', old, new) - end - - scope :with_expired_artifacts, ->() do - old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND artifacts_expire_at < ?], Time.now) - new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)], - Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND expire_at < ?', Time.now)) - where('ci_builds.id IN (? UNION ALL ?)', old, new) - end - + scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } + scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :ref_protected, -> { where(protected: true) } diff --git a/changelogs/unreleased/expired-ci-artifacts.yml b/changelogs/unreleased/expired-ci-artifacts.yml deleted file mode 100644 index 2fcbdb02f84..00000000000 --- a/changelogs/unreleased/expired-ci-artifacts.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Change SQL for expired artifacts to use new ci_job_artifacts.expire_at -merge_request: 16578 -author: -type: performance diff --git a/db/migrate/20180119160751_optimize_ci_job_artifacts.rb b/db/migrate/20180119160751_optimize_ci_job_artifacts.rb deleted file mode 100644 index 9b4340ed7b7..00000000000 --- a/db/migrate/20180119160751_optimize_ci_job_artifacts.rb +++ /dev/null @@ -1,23 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class OptimizeCiJobArtifacts < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # Set this constant to true if this migration requires downtime. - DOWNTIME = false - - disable_ddl_transaction! - - def up - # job_id is just here to be a covering index for index only scans - # since we'll almost always be joining against ci_builds on job_id - add_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id]) - add_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''") - end - - def down - remove_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id]) - remove_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''") - end -end diff --git a/db/schema.rb b/db/schema.rb index 6b43fc8403c..b281be110da 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -293,7 +293,6 @@ ActiveRecord::Schema.define(version: 20180208183958) do t.integer "failure_reason" end - add_index "ci_builds", ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree @@ -334,7 +333,6 @@ ActiveRecord::Schema.define(version: 20180208183958) do t.string "file" end - add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index f6ba3a581ca..6ba599cdf83 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -180,8 +180,8 @@ FactoryBot.define do trait :artifacts do after(:create) do |build| - create(:ci_job_artifact, :archive, job: build, expire_at: build.artifacts_expire_at) - create(:ci_job_artifact, :metadata, job: build, expire_at: build.artifacts_expire_at) + create(:ci_job_artifact, :archive, job: build) + create(:ci_job_artifact, :metadata, job: build) build.reload end end From 1a6dbaefb1948d81438f1d50f93deabcc59a43eb Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Feb 2018 14:26:21 -0800 Subject: [PATCH 054/161] Add back database changes for Ci::Build --- ...0180119160751_optimize_ci_job_artifacts.rb | 23 +++++++++++++++++++ db/schema.rb | 2 ++ spec/factories/ci/builds.rb | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20180119160751_optimize_ci_job_artifacts.rb diff --git a/db/migrate/20180119160751_optimize_ci_job_artifacts.rb b/db/migrate/20180119160751_optimize_ci_job_artifacts.rb new file mode 100644 index 00000000000..9b4340ed7b7 --- /dev/null +++ b/db/migrate/20180119160751_optimize_ci_job_artifacts.rb @@ -0,0 +1,23 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class OptimizeCiJobArtifacts < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + # job_id is just here to be a covering index for index only scans + # since we'll almost always be joining against ci_builds on job_id + add_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id]) + add_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''") + end + + def down + remove_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id]) + remove_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''") + end +end diff --git a/db/schema.rb b/db/schema.rb index b281be110da..6b43fc8403c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -293,6 +293,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do t.integer "failure_reason" end + add_index "ci_builds", ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree @@ -333,6 +334,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do t.string "file" end + add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 6ba599cdf83..f6ba3a581ca 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -180,8 +180,8 @@ FactoryBot.define do trait :artifacts do after(:create) do |build| - create(:ci_job_artifact, :archive, job: build) - create(:ci_job_artifact, :metadata, job: build) + create(:ci_job_artifact, :archive, job: build, expire_at: build.artifacts_expire_at) + create(:ci_job_artifact, :metadata, job: build, expire_at: build.artifacts_expire_at) build.reload end end From 8b727fea81b19f12366894c4a4b981250c549833 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 16 Feb 2018 15:15:32 +0100 Subject: [PATCH 055/161] Don't cache a nil repository root ref to prevent caching issues --- app/models/repository.rb | 8 ++------ changelogs/unreleased/dm-dont-cache-nil-root-ref.yml | 5 +++++ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/dm-dont-cache-nil-root-ref.yml diff --git a/app/models/repository.rb b/app/models/repository.rb index 4f754b11da4..299a3f32a85 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -492,12 +492,8 @@ class Repository end def root_ref - if raw_repository - raw_repository.root_ref - else - # When the repo does not exist we raise this error so no data is cached. - raise Gitlab::Git::Repository::NoRepository - end + # When the repo does not exist, or there is no root ref, we raise this error so no data is cached. + raw_repository&.root_ref or raise Gitlab::Git::Repository::NoRepository # rubocop:disable Style/AndOr end cache_method :root_ref diff --git a/changelogs/unreleased/dm-dont-cache-nil-root-ref.yml b/changelogs/unreleased/dm-dont-cache-nil-root-ref.yml new file mode 100644 index 00000000000..4dab7d0ffca --- /dev/null +++ b/changelogs/unreleased/dm-dont-cache-nil-root-ref.yml @@ -0,0 +1,5 @@ +--- +title: Don't cache a nil repository root ref to prevent caching issues +merge_request: +author: +type: fixed From 50cdf41e7e1bac48cb9d3e5414682e1bd59e5db9 Mon Sep 17 00:00:00 2001 From: Vicky Chijwani Date: Sat, 17 Feb 2018 22:17:21 +0530 Subject: [PATCH 056/161] Allow oxford commas and spaces before commas in MR issue closing pattern. --- config/initializers/1_settings.rb | 2 +- spec/lib/gitlab/closing_issue_extractor_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 28e05bfc18d..17a8801f7bc 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -262,7 +262,7 @@ Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled']. Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.__send__(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?: *,? +and +| *, *)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 28c679af12a..8d4862932b2 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -365,6 +365,20 @@ describe Gitlab::ClosingIssueExtractor do .to match_array([issue, other_issue, third_issue]) end + it 'allows oxford commas (comma before and) when referencing multiple issues' do + message = "Closes #{reference}, #{reference2}, and #{reference3}" + + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) + end + + it 'allows spaces before commas when referencing multiple issues' do + message = "Closes #{reference} , #{reference2} , and #{reference3}" + + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) + end + it 'fetches issues in multi-line message' do message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" From 3a9a80315537c48a2fb3c338e0f8c76b3f9aa06d Mon Sep 17 00:00:00 2001 From: Vicky Chijwani Date: Sat, 17 Feb 2018 23:18:31 +0530 Subject: [PATCH 057/161] Add changelog entry --- .../unreleased/17500-mr-multiple-issues-oxford-comma.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml diff --git a/changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml b/changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml new file mode 100644 index 00000000000..a94e6153a05 --- /dev/null +++ b/changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml @@ -0,0 +1,5 @@ +--- +title: Update issue closing pattern to allow variations in punctuation +merge_request: 17198 +author: Vicky Chijwani +type: changed From 46e6a9f8a0f2dc0ae4e3152646f319a7cb5abcb2 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 17 Feb 2018 21:29:22 -0800 Subject: [PATCH 058/161] Don't attempt to update user tracked fields if database is in read-only With Geo, attempting to view an endpoint with a user could result in an Error 500 since Devise attempts to update the last sign-in IP and other details. Closes gitlab-org/gitlab-ee#4972 --- app/models/user.rb | 2 ++ changelogs/unreleased/sh-guard-read-only-user-updates.yml | 5 +++++ spec/models/user_spec.rb | 8 ++++++++ 3 files changed, 15 insertions(+) create mode 100644 changelogs/unreleased/sh-guard-read-only-user-updates.yml diff --git a/app/models/user.rb b/app/models/user.rb index 5e84d2da805..f5eeba27572 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,6 +59,8 @@ class User < ActiveRecord::Base # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour def update_tracked_fields!(request) + return if Gitlab::Database.read_only? + update_tracked_fields(request) lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i) diff --git a/changelogs/unreleased/sh-guard-read-only-user-updates.yml b/changelogs/unreleased/sh-guard-read-only-user-updates.yml new file mode 100644 index 00000000000..b8dbd840ed9 --- /dev/null +++ b/changelogs/unreleased/sh-guard-read-only-user-updates.yml @@ -0,0 +1,5 @@ +--- +title: Don't attempt to update user tracked fields if database is in read-only +merge_request: +author: +type: fixed diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1815696a8a0..3531de244bd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -496,6 +496,14 @@ describe User do user2.update_tracked_fields!(request) end.to change { user2.reload.current_sign_in_at } end + + it 'does not write if the DB is in read-only mode' do + expect(Gitlab::Database).to receive(:read_only?).and_return(true) + + expect do + user.update_tracked_fields!(request) + end.not_to change { user.reload.current_sign_in_at } + end end shared_context 'user keys' do From 790ab5909b37a1eb64d7a86c21c6096441512290 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Wed, 14 Feb 2018 14:08:30 +0100 Subject: [PATCH 059/161] Remember assignee when moving an issue Related to #41949 --- app/services/issues/move_service.rb | 3 ++- changelogs/unreleased/41949-move.yml | 5 +++++ spec/services/issues/move_service_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/41949-move.yml diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 299b9c6215f..7140890d201 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -48,7 +48,8 @@ module Issues new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids, milestone_id: cloneable_milestone_id, project: @new_project, author: @old_issue.author, - description: rewrite_content(@old_issue.description) } + description: rewrite_content(@old_issue.description), + assignee_ids: @old_issue.assignee_ids } new_params = @old_issue.serializable_hash.symbolize_keys.merge(new_params) CreateService.new(@new_project, @current_user, new_params).execute diff --git a/changelogs/unreleased/41949-move.yml b/changelogs/unreleased/41949-move.yml new file mode 100644 index 00000000000..40ccac63a28 --- /dev/null +++ b/changelogs/unreleased/41949-move.yml @@ -0,0 +1,5 @@ +--- +title: Remember assignee when moving an issue +merge_request: +author: +type: fixed diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 322c91065e7..c148a98569b 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -232,6 +232,28 @@ describe Issues::MoveService do end end + context 'issue with assignee' do + let(:assignee) { create(:user) } + + before do + old_issue.assignees = [assignee] + end + + it 'preserves assignee with access to the new issue' do + new_project.add_reporter(assignee) + + new_issue = move_service.execute(old_issue, new_project) + + expect(new_issue.assignees).to eq([assignee]) + end + + it 'ignores assignee without access to the new issue' do + new_issue = move_service.execute(old_issue, new_project) + + expect(new_issue.assignees).to be_empty + end + end + context 'notes with references' do before do create(:merge_request, source_project: old_project) From 13074cab02fb20dd16eefd7c0539bfd6a341aa02 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 18 Feb 2018 13:48:28 -0600 Subject: [PATCH 060/161] Update two links in the changelog developer docs --- doc/development/changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/changelog.md b/doc/development/changelog.md index c1f783ce877..757987b2e7a 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -279,8 +279,8 @@ After much discussion we settled on the current solution of one file per entry, and then compiling the entries into the overall `CHANGELOG.md` file during the [release process]. -[boring solution]: https://about.gitlab.com/handbook/#boring-solutions -[release managers]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/release-manager.md +[boring solution]: https://about.gitlab.com/handbook/values/#boring-solutions +[release managers]: https://gitlab.com/gitlab-org/release/docs/blob/master/quickstart/release-manager.md [started brainstorming]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17826 [release process]: https://gitlab.com/gitlab-org/release-tools From 8d32dfef9b7eefe3d48dc63398315ba41879329c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 18 Feb 2018 21:45:51 -0800 Subject: [PATCH 061/161] Fix squash rebase not working when diff contained encoded data When the applied diff contains UTF-8 or some other encoded data, the diff returned back from the git process may be in ASCII-8BIT format. Writing this data to stdin may fail if the data because stdin expects this data to be in UTF-8. By switching the output to binmode, we ensure that the diff will always be written as-is. Closes gitlab-org/gitlab-ee#4960 --- .../unreleased/sh-fix-squash-rebase-utf8-data.yml | 5 +++++ lib/gitlab/git/repository.rb | 1 + spec/lib/gitlab/git/repository_spec.rb | 12 ++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml diff --git a/changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml b/changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml new file mode 100644 index 00000000000..c76b263152b --- /dev/null +++ b/changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml @@ -0,0 +1,5 @@ +--- +title: Fix squash rebase not working when diff contained encoded data +merge_request: +author: +type: fixed diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 5f014e43c6f..a10bc0dd32b 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -2195,6 +2195,7 @@ module Gitlab # Apply diff of the `diff_range` to the worktree diff = run_git!(%W(diff --binary #{diff_range})) run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin| + stdin.binmode stdin.write(diff) end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index edcf8889c27..0e9150964fa 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require "spec_helper" describe Gitlab::Git::Repository, seed_helper: true do @@ -2221,6 +2222,17 @@ describe Gitlab::Git::Repository, seed_helper: true do subject end end + + context 'with an ASCII-8BIT diff', :skip_gitaly_mock do + let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" } + + it 'applies a ASCII-8BIT diff' do + allow(repository).to receive(:run_git!).and_call_original + allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) + + expect(subject.length).to eq(40) + end + end end end From 15ca2d5fb5961399d8031fa3c9da818ffb9cbb0f Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 19 Feb 2018 00:02:13 -0600 Subject: [PATCH 062/161] Fix get a single pages domain when project path contains a period --- ...api-returns-404-when-using-a-specific-domain.yml | 5 +++++ lib/api/pages_domains.rb | 10 ++++++---- spec/requests/api/pages_domains_spec.rb | 13 ++++++++++++- 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml diff --git a/changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml b/changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml new file mode 100644 index 00000000000..d77572d6175 --- /dev/null +++ b/changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml @@ -0,0 +1,5 @@ +--- +title: Fix get a single pages domain when project path contains a period +merge_request: 17206 +author: Travis Miller +type: fixed diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index d7b613a717e..ba33993d852 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -2,6 +2,8 @@ module API class PagesDomains < Grape::API include PaginationParams + PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX) + before do authenticate! end @@ -48,7 +50,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: { id: %r{[^/]+} } do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do before do require_pages_enabled! end @@ -71,7 +73,7 @@ module API params do requires :domain, type: String, desc: 'The domain' end - get ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do + get ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do authorize! :read_pages, user_project present pages_domain, with: Entities::PagesDomain @@ -105,7 +107,7 @@ module API optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate' optional :key, allow_blank: false, types: [File, String], desc: 'The key' end - put ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do + put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do authorize! :update_pages, user_project pages_domain_params = declared(params, include_parent_namespaces: false) @@ -126,7 +128,7 @@ module API params do requires :domain, type: String, desc: 'The domain' end - delete ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do + delete ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do authorize! :update_pages, user_project status 204 diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index 5d01dc37f0e..025165622b7 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe API::PagesDomains do - set(:project) { create(:project) } + set(:project) { create(:project, path: 'my.project') } set(:user) { create(:user) } set(:admin) { create(:admin) } @@ -16,6 +16,7 @@ describe API::PagesDomains do let(:route) { "/projects/#{project.id}/pages/domains" } let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" } + let(:route_domain_path) { "/projects/#{project.path_with_namespace.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" } let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" } let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" } let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" } @@ -144,6 +145,16 @@ describe API::PagesDomains do expect(json_response['certificate']).to be_nil end + it 'returns pages domain with project path' do + get api(route_domain_path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') + expect(json_response['domain']).to eq(pages_domain.domain) + expect(json_response['url']).to eq(pages_domain.url) + expect(json_response['certificate']).to be_nil + end + it 'returns pages domain with a certificate' do get api(route_secure_domain, user) From fdad576838c16d0ae7d181e85a5889d8ae4e5014 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 19 Feb 2018 00:21:47 -0800 Subject: [PATCH 063/161] Fix Error 500 when viewing a commit with a GPG signature in Geo Closes gitlab-org/gitlab-ee#4825 --- .../sh-fix-geo-error-500-gpg-commit.yml | 5 ++++ lib/gitlab/gpg/commit.rb | 4 ++- spec/lib/gitlab/gpg/commit_spec.rb | 26 ++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml diff --git a/changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml b/changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml new file mode 100644 index 00000000000..5b4bbe0dc7a --- /dev/null +++ b/changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml @@ -0,0 +1,5 @@ +--- +title: Fix Error 500 when viewing a commit with a GPG signature in Geo +merge_request: +author: +type: fixed diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 672b5579dfd..90dd569aaf8 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -60,7 +60,9 @@ module Gitlab def create_cached_signature! using_keychain do |gpg_key| - GpgSignature.create!(attributes(gpg_key)) + signature = GpgSignature.new(attributes(gpg_key)) + signature.save! unless Gitlab::Database.read_only? + signature end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index e3bf2801406..67c62458f0f 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -49,7 +49,9 @@ describe Gitlab::Gpg::Commit do end it 'returns a valid signature' do - expect(described_class.new(commit).signature).to have_attributes( + signature = described_class.new(commit).signature + + expect(signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: gpg_key, @@ -58,9 +60,31 @@ describe Gitlab::Gpg::Commit do gpg_key_user_email: GpgHelpers::User1.emails.first, verification_status: 'verified' ) + expect(signature.persisted?).to be_truthy end it_behaves_like 'returns the cached signature on second call' + + context 'read-only mode' do + before do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + end + + it 'does not create a cached signature' do + signature = described_class.new(commit).signature + + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'verified' + ) + expect(signature.persisted?).to be_falsey + end + end end context 'commit signed with a subkey' do From d4dfa342c1f5a916d325e198ff19d3f702bfb3d9 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Thu, 15 Feb 2018 05:21:17 +0000 Subject: [PATCH 064/161] Avoid slow File Lock checks when not used Also avoid double commit lookup during file lock check by reusing memoized commits. --- lib/gitlab/checks/change_access.rb | 7 ++++++- lib/gitlab/checks/commit_check.rb | 4 ++-- spec/lib/gitlab/checks/change_access_spec.rb | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index d75e73dac10..94d45c17ca0 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -120,6 +120,7 @@ module Gitlab def commits_check return if deletion? || newrev.nil? + return unless should_run_commit_validations? # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593 ::Gitlab::GitalyClient.allow_n_plus_1_calls do @@ -138,6 +139,10 @@ module Gitlab private + def should_run_commit_validations? + commit_check.validate_lfs_file_locks? + end + def updated_from_web? protocol == 'web' end @@ -175,7 +180,7 @@ module Gitlab end def commits - project.repository.new_commits(newrev) + @commits ||= project.repository.new_commits(newrev) end end end diff --git a/lib/gitlab/checks/commit_check.rb b/lib/gitlab/checks/commit_check.rb index ae0cd142378..43a52b493bb 100644 --- a/lib/gitlab/checks/commit_check.rb +++ b/lib/gitlab/checks/commit_check.rb @@ -35,14 +35,14 @@ module Gitlab end end - private - def validate_lfs_file_locks? strong_memoize(:validate_lfs_file_locks) do project.lfs_enabled? && project.lfs_file_locks.any? && newrev && oldrev end end + private + def lfs_file_locks_validation lambda do |paths| lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user.id).first diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 475b5c5cfb2..b49ddbfc780 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -190,7 +190,7 @@ describe Gitlab::Checks::ChangeAccess do context 'with LFS not enabled' do it 'skips the validation' do - expect_any_instance_of(described_class).not_to receive(:lfs_file_locks_validation) + expect_any_instance_of(Gitlab::Checks::CommitCheck).not_to receive(:validate) subject.exec end @@ -207,7 +207,7 @@ describe Gitlab::Checks::ChangeAccess do end end - context 'when change is sent by the author od the lock' do + context 'when change is sent by the author of the lock' do let(:user) { owner } it "doesn't raise any error" do From 0c325ce8f5bb934033448ccee0ac3e4105d68385 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 19 Feb 2018 10:09:49 +0000 Subject: [PATCH 065/161] Clarify changelog for squash encoding fix [ci skip] --- changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml b/changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml index c76b263152b..aa43487d741 100644 --- a/changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml +++ b/changelogs/unreleased/sh-fix-squash-rebase-utf8-data.yml @@ -1,5 +1,5 @@ --- -title: Fix squash rebase not working when diff contained encoded data +title: Fix squash not working when diff contained non-ASCII data merge_request: author: type: fixed From 23dd313d76f2254a7a5c58283cb76236a160647b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 19 Feb 2018 11:21:23 +0000 Subject: [PATCH 066/161] Convert Gitaly commit parent IDs to array as early as possible The tracking issue if this causes problems is https://gitlab.com/gitlab-org/gitaly/issues/1028 --- lib/gitlab/git/commit.rb | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index ea59978c58a..ae27a138b7c 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -471,15 +471,6 @@ module Gitlab private - def parent_ids=(shas) - @parent_ids = case shas - when String - JSON.parse(shas) - else - shas - end - end - def init_from_hash(hash) raw_commit = hash.symbolize_keys @@ -517,7 +508,7 @@ module Gitlab @committed_date = Time.at(commit.committer.date.seconds).utc @committer_name = commit.committer.name.dup @committer_email = commit.committer.email.dup - @parent_ids = commit.parent_ids + @parent_ids = Array(commit.parent_ids) end def serialize_keys From 835e0227d8b04dd60a03aa7d089f3f4401f819f7 Mon Sep 17 00:00:00 2001 From: Norike Abe Date: Mon, 19 Feb 2018 11:21:48 +0000 Subject: [PATCH 067/161] emoji.codes is now emojicopy.com --- doc/user/markdown.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/markdown.md b/doc/user/markdown.md index b590dfa0d40..ac8ff67f622 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -226,7 +226,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. - Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup: + Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: @@ -236,7 +236,7 @@ You can use it to point out a :bug: or warn about :speak_no_evil: patches. And i If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. -Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup: +Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: ### Special GitLab References From ef99a3c8bc09e9f03ee9aec5c07bfeca6b7d7adf Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 19 Feb 2018 14:27:39 +0100 Subject: [PATCH 068/161] Increase feature flag cache TTL to one hour Flipper already takes care of flushing cache entries when enabling/disabling features so it should be safe to increase the TTL. This in turn should drastically reduce the number of Flipper queries executed. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/40854 --- changelogs/unreleased/flipper-caching.yml | 5 +++++ config/initializers/flipper.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/flipper-caching.yml diff --git a/changelogs/unreleased/flipper-caching.yml b/changelogs/unreleased/flipper-caching.yml new file mode 100644 index 00000000000..6db27fd579e --- /dev/null +++ b/changelogs/unreleased/flipper-caching.yml @@ -0,0 +1,5 @@ +--- +title: Increase feature flag cache TTL to one hour +merge_request: +author: +type: performance diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index cc9167d29b9..c60ad535fd5 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -8,7 +8,7 @@ Flipper.configure do |config| cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new( adapter, Rails.cache, - expires_in: 10.seconds) + expires_in: 1.hour) Flipper.new(cached_adapter) end From 6be8cd2293348ac70e6ed6bfc047823675b10d98 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 19 Feb 2018 14:22:00 +0000 Subject: [PATCH 069/161] specify date format --- doc/development/writing_documentation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index ceb0cdbb742..700039392ef 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -257,10 +257,10 @@ through the process of how to use it systematically. Every **Technical Article** contains a frontmatter at the beginning of the doc with the following information: -- **Type of article** (user guide, admin guide, tech overview, tutorial) +- **Type of article** (user guide, admin guide, technical overview, tutorial) - **Knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) - **Author's name** and **GitLab.com handle** -- **Publication date** +- **Publication date** (ISO format YYYY-MM-DD) For example: From cca0f0a5dae0cf561f4491c71483aec08151b9dc Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Feb 2018 21:59:05 +0530 Subject: [PATCH 070/161] Fix single digit value clipping --- app/assets/stylesheets/framework/stacked_progress_bar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss index 4869cda73e5..528ba53a48b 100644 --- a/app/assets/stylesheets/framework/stacked_progress_bar.scss +++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss @@ -10,7 +10,7 @@ .status-neutral, .status-red, { height: 100%; - min-width: 25px; + min-width: 30px; padding: 0 5px; font-size: $tooltip-font-size; font-weight: normal; From af49e61e0d501f10299f567fec01d9da2dcebf3a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Feb 2018 22:04:06 +0530 Subject: [PATCH 071/161] Add changelog entry --- .../kp-fix-stacked-bar-progress-value-clipping.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml diff --git a/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml b/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml new file mode 100644 index 00000000000..690536a533b --- /dev/null +++ b/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml @@ -0,0 +1,5 @@ +--- +title: Fix single digit value clipping for stacked progress bar +merge_request: 17217 +author: +type: fixed From e345406cbc30e729d08be2fee06db331e69438b6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 19 Feb 2018 11:00:05 -0600 Subject: [PATCH 072/161] Remove import so that webpack can handle the module --- app/assets/javascripts/dispatcher.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 7e750d15d3d..008b68eb661 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -106,9 +106,6 @@ var Dispatcher; .catch(fail); break; case 'projects:merge_requests:show': - import('./pages/projects/merge_requests/show') - .then(callDefault) - .catch(fail); shortcut_handler = true; break; case 'dashboard:activity': From c2440a9b8c7a4eec9dffeaae4b10d50cff907e6a Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 20 Feb 2018 04:10:26 +1030 Subject: [PATCH 073/161] 35418 - remove avatar underline 35418 - update changelog --- app/assets/stylesheets/pages/milestone.scss | 4 ++++ app/views/shared/milestones/_issuable.html.haml | 2 +- changelogs/unreleased/35418-remove-underline-for-avatar.yml | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/35418-remove-underline-for-avatar.yml diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index ae8fa45a2d7..e5afa8fffcb 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -115,6 +115,10 @@ display: block; margin-top: 7px; + .issue-link { + display: inline-block; + } + .issuable-number { color: $gl-text-color-secondary; margin-right: 5px; diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 479b7270b28..129f6ab604e 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -17,7 +17,7 @@ = confidential_icon(issuable) = link_to issuable.title, issuable_url_args, title: issuable.title .issuable-detail - = link_to [namespace, project, issuable] do + = link_to [namespace, project, issuable], class: 'issue-link' do %span.issuable-number= issuable.to_reference - labels.each do |label| diff --git a/changelogs/unreleased/35418-remove-underline-for-avatar.yml b/changelogs/unreleased/35418-remove-underline-for-avatar.yml new file mode 100644 index 00000000000..034365e1137 --- /dev/null +++ b/changelogs/unreleased/35418-remove-underline-for-avatar.yml @@ -0,0 +1,5 @@ +--- +title: remove avater underline +merge_request: 17219 +author: Ken Ding +type: fixed From 80090c8b06859ac23fec4b69c88a0e854ca2db8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=99=88=20=20jacopo=20beschi=20=F0=9F=99=89?= Date: Mon, 19 Feb 2018 17:47:08 +0000 Subject: [PATCH 074/161] Resolve "group request membership mail with too long list of "To:"" --- app/mailers/emails/members.rb | 11 +-- app/services/notification_service.rb | 17 +++- ...2274-group-request-membership-long-too.yml | 5 ++ spec/mailers/notify_spec.rb | 70 +++++---------- spec/services/notification_service_spec.rb | 89 ++++++++++++++----- 5 files changed, 114 insertions(+), 78 deletions(-) create mode 100644 changelogs/unreleased/42274-group-request-membership-long-too.yml diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index d76c61c369f..75cf56a51f2 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -7,18 +7,11 @@ module Emails helper_method :member_source, :member end - def member_access_requested_email(member_source_type, member_id) + def member_access_requested_email(member_source_type, member_id, recipient_notification_email) @member_source_type = member_source_type @member_id = member_id - admins = member_source.members.owners_and_masters.pluck(:notification_email) - # A project in a group can have no explicit owners/masters, in that case - # we fallbacks to the group's owners/masters. - if admins.empty? && member_source.respond_to?(:group) && member_source.group - admins = member_source.group.members.owners_and_masters.pluck(:notification_email) - end - - mail(to: admins, + mail(to: recipient_notification_email, subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}")) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 8c84ccfcc92..56e941d90ff 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -208,7 +208,12 @@ class NotificationService def new_access_request(member) return true unless member.notifiable?(:subscription) - mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later + recipients = member.source.members.owners_and_masters + if fallback_to_group_owners_masters?(recipients, member) + recipients = member.source.group.members.owners_and_masters + end + + recipients.each { |recipient| deliver_access_request_email(recipient, member) } end def decline_access_request(member) @@ -435,4 +440,14 @@ class NotificationService def notifiable_users(*args) NotificationRecipientService.notifiable_users(*args) end + + def deliver_access_request_email(recipient, member) + mailer.member_access_requested_email(member.real_source_type, member.id, recipient.user.notification_email).deliver_later + end + + def fallback_to_group_owners_masters?(recipients, member) + return false if recipients.present? + + member.source.respond_to?(:group) && member.source.group + end end diff --git a/changelogs/unreleased/42274-group-request-membership-long-too.yml b/changelogs/unreleased/42274-group-request-membership-long-too.yml new file mode 100644 index 00000000000..03efedba638 --- /dev/null +++ b/changelogs/unreleased/42274-group-request-membership-long-too.yml @@ -0,0 +1,5 @@ +--- +title: Fix long list of recipients on group request membership email +merge_request: 17121 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 59eda025108..bcbb9287199 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -463,59 +463,30 @@ describe Notify do end describe 'project access requested' do - context 'for a project in a user namespace' do - let(:project) do - create(:project, :public, :access_requestable) do |project| - project.add_master(project.owner, current_user: project.owner) - end - end - - let(:project_member) do - project.request_access(user) - project.requesters.find_by(user_id: user.id) - end - subject { described_class.member_access_requested_email('project', project_member.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" - - it 'contains all the useful information' do - to_emails = subject.header[:to].addrs - expect(to_emails.size).to eq(1) - expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) - - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace - is_expected.to have_body_text project_project_members_url(project) - is_expected.to have_body_text project_member.human_access + let(:project) do + create(:project, :public, :access_requestable) do |project| + project.add_master(project.owner) end end - context 'for a project in a group' do - let(:group_owner) { create(:user) } - let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } - let(:project) { create(:project, :public, :access_requestable, namespace: group) } - let(:project_member) do - project.request_access(user) - project.requesters.find_by(user_id: user.id) - end - subject { described_class.member_access_requested_email('project', project_member.id) } + let(:project_member) do + project.request_access(user) + project.requesters.find_by(user_id: user.id) + end + subject { described_class.member_access_requested_email('project', project_member.id, recipient.notification_email) } - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" - it 'contains all the useful information' do - to_emails = subject.header[:to].addrs - expect(to_emails.size).to eq(1) - expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) + it 'contains all the useful information' do + to_emails = subject.header[:to].addrs.map(&:address) + expect(to_emails).to eq([recipient.notification_email]) - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace - is_expected.to have_body_text project_project_members_url(project) - is_expected.to have_body_text project_member.human_access - end + is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" + is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_body_text project_project_members_url(project) + is_expected.to have_body_text project_member.human_access end end @@ -959,13 +930,16 @@ describe Notify do group.request_access(user) group.requesters.find_by(user_id: user.id) end - subject { described_class.member_access_requested_email('group', group_member.id) } + subject { described_class.member_access_requested_email('group', group_member.id, recipient.notification_email) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do + to_emails = subject.header[:to].addrs.map(&:address) + expect(to_emails).to eq([recipient.notification_email]) + is_expected.to have_subject "Request to join the #{group.name} group" is_expected.to have_html_escaped_body_text group.name is_expected.to have_body_text group_group_members_url(group) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 35eb84e5e88..836ffb7cea0 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1307,6 +1307,33 @@ describe NotificationService, :mailer do end describe 'GroupMember' do + let(:added_user) { create(:user) } + + describe '#new_access_request' do + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:developer) { create(:user) } + let!(:group) do + create(:group, :public, :access_requestable) do |group| + group.add_owner(owner) + group.add_master(master) + group.add_developer(developer) + end + end + + before do + reset_delivered_emails! + end + + it 'sends notification to group owners_and_masters' do + group.request_access(added_user) + + should_email(owner) + should_email(master) + should_not_email(developer) + end + end + describe '#decline_group_invite' do let(:creator) { create(:user) } let(:group) { create(:group) } @@ -1328,18 +1355,9 @@ describe NotificationService, :mailer do describe '#new_group_member' do let(:group) { create(:group) } - let(:added_user) { create(:user) } - - def create_member! - GroupMember.create( - group: group, - user: added_user, - access_level: Gitlab::Access::GUEST - ) - end it 'sends a notification' do - create_member! + group.add_guest(added_user) should_only_email(added_user) end @@ -1349,7 +1367,7 @@ describe NotificationService, :mailer do end it 'does not send a notification' do - create_member! + group.add_guest(added_user) should_not_email_anyone end end @@ -1357,8 +1375,42 @@ describe NotificationService, :mailer do end describe 'ProjectMember' do + let(:project) { create(:project) } + set(:added_user) { create(:user) } + + describe '#new_access_request' do + context 'for a project in a user namespace' do + let(:project) do + create(:project, :public, :access_requestable) do |project| + project.add_master(project.owner) + end + end + + it 'sends notification to project owners_and_masters' do + project.request_access(added_user) + + should_only_email(project.owner) + end + end + + context 'for a project in a group' do + let(:group_owner) { create(:user) } + let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } + let!(:project) { create(:project, :public, :access_requestable, namespace: group) } + + before do + reset_delivered_emails! + end + + it 'sends notification to group owners_and_masters' do + project.request_access(added_user) + + should_only_email(group_owner) + end + end + end + describe '#decline_group_invite' do - let(:project) { create(:project) } let(:member) { create(:user) } before do @@ -1375,19 +1427,12 @@ describe NotificationService, :mailer do end describe '#new_project_member' do - let(:project) { create(:project) } - let(:added_user) { create(:user) } - - def create_member! - create(:project_member, user: added_user, project: project) - end - it do create_member! should_only_email(added_user) end - describe 'when notifications are disabled' do + context 'when notifications are disabled' do before do create_global_setting_for(added_user, :disabled) end @@ -1398,6 +1443,10 @@ describe NotificationService, :mailer do end end end + + def create_member! + create(:project_member, user: added_user, project: project) + end end context 'guest user in private project' do From 69b750aee9f51f1206495531e14c1f9b6d421600 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 19 Feb 2018 17:56:33 +0000 Subject: [PATCH 075/161] [GH Import] Create an empty wiki if wiki import failed --- changelogs/unreleased/4826-github-import-wiki-fix-1.yml | 5 +++++ lib/gitlab/github_import/importer/repository_importer.rb | 1 + .../github_import/importer/repository_importer_spec.rb | 8 ++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/4826-github-import-wiki-fix-1.yml diff --git a/changelogs/unreleased/4826-github-import-wiki-fix-1.yml b/changelogs/unreleased/4826-github-import-wiki-fix-1.yml new file mode 100644 index 00000000000..69145cb6daf --- /dev/null +++ b/changelogs/unreleased/4826-github-import-wiki-fix-1.yml @@ -0,0 +1,5 @@ +--- +title: "[GitHub Import] Create an empty wiki if wiki import failed" +merge_request: +author: +type: fixed diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb index 7dd68a0d1cd..ab0b751fe24 100644 --- a/lib/gitlab/github_import/importer/repository_importer.rb +++ b/lib/gitlab/github_import/importer/repository_importer.rb @@ -63,6 +63,7 @@ module Gitlab true rescue Gitlab::Shell::Error => e if e.message !~ /repository not exported/ + project.create_wiki fail_import("Failed to import the wiki: #{e.message}") else true diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 46a57e08963..5bedfc79dd3 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -11,7 +11,8 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do import_source: 'foo/bar', repository_storage_path: 'foo', disk_path: 'foo', - repository: repository + repository: repository, + create_wiki: true ) end @@ -192,7 +193,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do expect(importer.import_wiki_repository).to eq(true) end - it 'marks the import as failed if an error was raised' do + it 'marks the import as failed and creates an empty repo if an error was raised' do expect(importer.gitlab_shell) .to receive(:import_repository) .and_raise(Gitlab::Shell::Error) @@ -201,6 +202,9 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do .to receive(:fail_import) .and_return(false) + expect(project) + .to receive(:create_wiki) + expect(importer.import_wiki_repository).to eq(false) end end From f23a57b0c5a45ab16463199ebf2dac2da3f0287f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 19 Feb 2018 12:40:18 -0600 Subject: [PATCH 076/161] Remove extraneous tests from Issues API spec These were all testing an implementation detail of `Issues::UpdateService` which is already well-tested and doesn't need to be re-tested here. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/25201 --- spec/requests/api/issues_spec.rb | 26 +------------------------- spec/requests/api/v3/issues_spec.rb | 26 +------------------------- 2 files changed, 2 insertions(+), 50 deletions(-) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 13bb3b92085..e6d7b9fde02 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Issues, :mailer do +describe API::Issues do set(:user) { create(:user) } set(:project) do create(:project, :public, creator_id: user.id, namespace: user.namespace) @@ -932,18 +932,6 @@ describe API::Issues, :mailer do expect(json_response['error']).to eq('confidential is invalid') end - it "sends notifications for subscribers of newly added labels" do - label = project.labels.first - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - post api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: label.title - end - - should_email(user2) - end - it "returns a 400 bad request if title not given" do post api("/projects/#{project.id}/issues", user), labels: 'label, label2' expect(response).to have_gitlab_http_status(400) @@ -1246,18 +1234,6 @@ describe API::Issues, :mailer do expect(json_response['labels']).to eq([label.title]) end - it "sends notifications for subscribers of newly added labels when issue is updated" do - label = create(:label, title: 'foo', color: '#FFAABB', project: project) - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - title: 'updated title', labels: label.title - end - - should_email(user2) - end - it 'removes all labels' do put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: '' diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 25b4c8438e4..0e745c82395 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::V3::Issues, :mailer do +describe API::V3::Issues do set(:user) { create(:user) } set(:user2) { create(:user) } set(:non_member) { create(:user) } @@ -780,18 +780,6 @@ describe API::V3::Issues, :mailer do expect(json_response['error']).to eq('confidential is invalid') end - it "sends notifications for subscribers of newly added labels" do - label = project.labels.first - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: label.title - end - - should_email(user2) - end - it "returns a 400 bad request if title not given" do post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2' @@ -1045,18 +1033,6 @@ describe API::V3::Issues, :mailer do expect(json_response['labels']).to eq([label.title]) end - it "sends notifications for subscribers of newly added labels when issue is updated" do - label = create(:label, title: 'foo', color: '#FFAABB', project: project) - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title', labels: label.title - end - - should_email(user2) - end - it 'removes all labels' do put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' From 834f455b43909525609a573df3153a413ef3c6d4 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 19 Feb 2018 20:43:10 +0000 Subject: [PATCH 077/161] Chart.html.haml refactor --- .../javascripts/graphs/graphs_bundle.js | 4 - .../projects/graphs/charts/index.js} | 2 +- .../pages/projects/pipelines/charts/index.js | 2 +- .../devise/sessions/two_factor.html.haml | 2 +- app/views/profiles/_head.html.haml | 2 +- .../profiles/two_factor_auths/show.html.haml | 4 +- app/views/projects/blob/_upload.html.haml | 2 +- app/views/projects/blob/edit.html.haml | 2 +- app/views/projects/blob/new.html.haml | 2 +- .../projects/blob/viewers/_balsamiq.html.haml | 2 +- .../projects/blob/viewers/_notebook.html.haml | 4 +- .../projects/blob/viewers/_pdf.html.haml | 4 +- .../projects/blob/viewers/_sketch.html.haml | 4 +- .../projects/blob/viewers/_stl.html.haml | 2 +- .../projects/commit/_pipelines_list.haml | 4 +- app/views/projects/commit/show.html.haml | 4 +- .../projects/cycle_analytics/show.html.haml | 4 +- .../projects/environments/folder.html.haml | 4 +- .../projects/environments/index.html.haml | 4 +- .../projects/environments/terminal.html.haml | 2 +- app/views/projects/graphs/charts.html.haml | 2 - app/views/projects/graphs/show.html.haml | 1 - app/views/projects/issues/show.html.haml | 4 +- .../merge_requests/conflicts.html.haml | 4 +- .../merge_requests/conflicts/show.html.haml | 4 +- app/views/projects/network/show.html.haml | 2 +- app/views/projects/pipelines/index.html.haml | 4 +- .../protected_branches/_index.html.haml | 2 +- .../projects/protected_tags/_index.html.haml | 2 +- .../registry/repositories/index.html.haml | 4 +- .../settings/repository/show.html.haml | 4 +- app/views/shared/issuable/_sidebar.html.haml | 4 +- app/views/shared/snippets/_form.html.haml | 2 +- config/webpack.config.js | 3 - package.json | 1 + vendor/assets/javascripts/Chart.js | 3477 ----------------- yarn.lock | 4 + 37 files changed, 51 insertions(+), 3533 deletions(-) delete mode 100644 app/assets/javascripts/graphs/graphs_bundle.js rename app/assets/javascripts/{graphs/graphs_charts.js => pages/projects/graphs/charts/index.js} (98%) delete mode 100644 vendor/assets/javascripts/Chart.js diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js deleted file mode 100644 index 534bc535bb6..00000000000 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ /dev/null @@ -1,4 +0,0 @@ -import Chart from 'vendor/Chart'; - -// export to global scope -window.Chart = Chart; diff --git a/app/assets/javascripts/graphs/graphs_charts.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js similarity index 98% rename from app/assets/javascripts/graphs/graphs_charts.js rename to app/assets/javascripts/pages/projects/graphs/charts/index.js index ec6eab34989..42df19c2968 100644 --- a/app/assets/javascripts/graphs/graphs_charts.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,4 +1,4 @@ -import Chart from 'vendor/Chart'; +import Chart from 'chart.js'; import _ from 'underscore'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index c1dafda0e24..bb92f4e1459 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -1,4 +1,4 @@ -import Chart from 'vendor/Chart'; +import Chart from 'chart.js'; const options = { scaleOverlay: true, diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index a039756c7e2..56ec1b3db0d 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,6 +1,6 @@ - if inject_u2f_api? - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('u2f') + = webpack_bundle_tag('u2f') %div = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication' diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml index 83ae9129807..a8eb66ca13c 100644 --- a/app/views/profiles/_head.html.haml +++ b/app/views/profiles/_head.html.haml @@ -1,2 +1,2 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('profile') + = webpack_bundle_tag('profile') diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 5207dac3ac2..e58cd20402c 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -6,8 +6,8 @@ - content_for :page_specific_javascripts do - if inject_u2f_api? - = page_specific_javascript_bundle_tag('u2f') - = page_specific_javascript_bundle_tag('two_factor_auth') + = webpack_bundle_tag('u2f') + = webpack_bundle_tag('two_factor_auth') .js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path } .row.prepend-top-default diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 21b6aa4bad9..16c56ea604a 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -29,4 +29,4 @@ = commit_in_fork_help - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('blob') + = webpack_bundle_tag('blob') diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 626cbc9e41d..60bd1c2528a 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -3,7 +3,7 @@ - page_title "Edit", @blob.path, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_bundle_tag('blob') + = webpack_bundle_tag('blob') %div{ class: container_class } - if @conflict diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index a4263774dfd..4e4288390f5 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -2,7 +2,7 @@ - page_title "New File", @path.presence, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_bundle_tag('blob') + = webpack_bundle_tag('blob') .editor-title-row %h3.page-title.blob-new-page-title New file diff --git a/app/views/projects/blob/viewers/_balsamiq.html.haml b/app/views/projects/blob/viewers/_balsamiq.html.haml index 1e7c461f02e..15349387eb2 100644 --- a/app/views/projects/blob/viewers/_balsamiq.html.haml +++ b/app/views/projects/blob/viewers/_balsamiq.html.haml @@ -1,4 +1,4 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('balsamiq_viewer') + = webpack_bundle_tag('balsamiq_viewer') .file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: blob_raw_path } } diff --git a/app/views/projects/blob/viewers/_notebook.html.haml b/app/views/projects/blob/viewers/_notebook.html.haml index 8a41bc53004..d1ffaca35b9 100644 --- a/app/views/projects/blob/viewers/_notebook.html.haml +++ b/app/views/projects/blob/viewers/_notebook.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('notebook_viewer') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('notebook_viewer') .file-content#js-notebook-viewer{ data: { endpoint: blob_raw_path } } diff --git a/app/views/projects/blob/viewers/_pdf.html.haml b/app/views/projects/blob/viewers/_pdf.html.haml index ec2b18bd4ab..fc3f0d922b1 100644 --- a/app/views/projects/blob/viewers/_pdf.html.haml +++ b/app/views/projects/blob/viewers/_pdf.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('pdf_viewer') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('pdf_viewer') .file-content#js-pdf-viewer{ data: { endpoint: blob_raw_path } } diff --git a/app/views/projects/blob/viewers/_sketch.html.haml b/app/views/projects/blob/viewers/_sketch.html.haml index 775e4584f77..8fb67c819c1 100644 --- a/app/views/projects/blob/viewers/_sketch.html.haml +++ b/app/views/projects/blob/viewers/_sketch.html.haml @@ -1,6 +1,6 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('sketch_viewer') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('sketch_viewer') .file-content#js-sketch-viewer{ data: { endpoint: blob_raw_path } } .js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' } diff --git a/app/views/projects/blob/viewers/_stl.html.haml b/app/views/projects/blob/viewers/_stl.html.haml index 6578d826ace..e58809ec008 100644 --- a/app/views/projects/blob/viewers/_stl.html.haml +++ b/app/views/projects/blob/viewers/_stl.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('stl_viewer') + = webpack_bundle_tag('stl_viewer') .file-content.is-stl-loading .text-center#js-stl-viewer{ data: { endpoint: blob_raw_path } } diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 49b0b314e1d..3f699882c5f 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -8,5 +8,5 @@ } } - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('commit_pipelines') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('commit_pipelines') diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 2890e9d2b65..4058e61eb9a 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -7,8 +7,8 @@ - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('diff_notes') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('diff_notes') .container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 71d30da14a9..d98e0564da4 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,8 +1,8 @@ - @no_container = true - page_title "Cycle Analytics" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('cycle_analytics') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('cycle_analytics') #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index d9c9f0ed546..eca10d99908 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -2,8 +2,8 @@ - page_title "Environments" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag("environments_folder") + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag("environments_folder") #environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json), "folder-name" => @folder, diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 88f1348da47..31cf173fa9c 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,8 +3,8 @@ - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag("common_vue") - = page_specific_javascript_bundle_tag("environments") + = webpack_bundle_tag("common_vue") + = webpack_bundle_tag("environments") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index a073a164f11..7be4ef39117 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -3,7 +3,7 @@ - content_for :page_specific_javascripts do = stylesheet_link_tag "xterm/xterm" - = page_specific_javascript_bundle_tag("terminal") + = webpack_bundle_tag("terminal") %div{ class: container_class } .top-area diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 300a39fe257..efdb494e1ae 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -2,8 +2,6 @@ - page_title "Charts" - content_for :page_specific_javascripts do = webpack_bundle_tag('common_d3') - = webpack_bundle_tag('graphs') - = webpack_bundle_tag('graphs_charts') .repo-charts{ class: container_class } %h4.sub-header diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index cce16bc58b3..91d2c48ccd1 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -2,7 +2,6 @@ - page_title _('Contributors') - content_for :page_specific_javascripts do = webpack_bundle_tag('common_d3') - = webpack_bundle_tag('graphs') = webpack_bundle_tag('graphs_show') .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 1f28d8acff6..b9dd4c27e63 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -87,5 +87,5 @@ = render 'shared/issuable/sidebar', issuable: @issue -= page_specific_javascript_bundle_tag('common_vue') -= page_specific_javascript_bundle_tag('issue_show') += webpack_bundle_tag('common_vue') += webpack_bundle_tag('issue_show') diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 454bc359b6b..2a2e57027be 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,7 +1,7 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('merge_conflicts') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/mr_title" diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml index 454bc359b6b..2a2e57027be 100644 --- a/app/views/projects/merge_requests/conflicts/show.html.haml +++ b/app/views/projects/merge_requests/conflicts/show.html.haml @@ -1,7 +1,7 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('merge_conflicts') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/mr_title" diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 2efb7fc719f..97be8950db0 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title "Graph" - page_title "Graph", @ref - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('network') + = webpack_bundle_tag('network') = render "head" %div{ class: container_class } .project-network diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index f8555f11aab..fdcc60f48a5 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -13,5 +13,5 @@ "ci-lint-path" => ci_lint_path, "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('pipelines') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('pipelines') diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml index 2f30fe33a97..127a338e413 100644 --- a/app/views/projects/protected_branches/_index.html.haml +++ b/app/views/projects/protected_branches/_index.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('protected_branches') + = webpack_bundle_tag('protected_branches') - content_for :create_protected_branch do = render 'projects/protected_branches/create_protected_branch' diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml index 955220562a6..74f7f63c941 100644 --- a/app/views/projects/protected_tags/_index.html.haml +++ b/app/views/projects/protected_tags/_index.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('protected_tags') + = webpack_bundle_tag('protected_tags') - content_for :create_protected_tag do = render 'projects/protected_tags/create_protected_tag' diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 36ea5e013e4..744b88760bc 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -14,8 +14,8 @@ .col-lg-12 #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } } - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('registry_list') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('registry_list') .row.prepend-top-10 .col-lg-12 diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 517d51993d2..3077203c2a6 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -3,8 +3,8 @@ - @content_class = "limit-container-width" unless fluid_layout - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('deploy_keys') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('deploy_keys') -# Protected branches & tags use a lot of nested partials. -# The shared parts of the views can be found in the `shared` directory. diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 15fd01c8429..dc583d3eb3b 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,7 +1,7 @@ - todo = issuable_todo(issuable) - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('sidebar') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('sidebar') %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 43322978749..2726a4934fb 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,6 +1,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_bundle_tag('snippet') + = webpack_bundle_tag('snippet') .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f| diff --git a/config/webpack.config.js b/config/webpack.config.js index f81f7067258..225907a544b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -62,8 +62,6 @@ var config = { environments: './environments/environments_bundle.js', environments_folder: './environments/folder/environments_folder_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', - graphs: './graphs/graphs_bundle.js', - graphs_charts: './graphs/graphs_charts.js', graphs_show: './graphs/graphs_show.js', help: './help/help.js', how_to_merge: './how_to_merge.js', @@ -283,7 +281,6 @@ var config = { new webpack.optimize.CommonsChunkPlugin({ name: 'common_d3', chunks: [ - 'graphs', 'graphs_show', 'monitoring', 'users', diff --git a/package.json b/package.json index 4eccd51a3a6..7498bb486dc 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "blackst0ne-mermaid": "^7.1.0-fixed", "bootstrap-sass": "^3.3.6", "brace-expansion": "^1.1.8", + "chart.js": "1.0.2", "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", "compression-webpack-plugin": "^1.0.0", diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js deleted file mode 100644 index c264262ba73..00000000000 --- a/vendor/assets/javascripts/Chart.js +++ /dev/null @@ -1,3477 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * Version: 1.0.2 - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - - -(function(){ - - "use strict"; - - //Declare root variable - window in the browser, global on the server - var root = this, - previous = root.Chart; - - //Occupy the global variable of Chart, and create a simple base class - var Chart = function(context){ - var chart = this; - this.canvas = context.canvas; - - this.ctx = context; - - //Variables global to the chart - var computeDimension = function(element,dimension) - { - if (element['offset'+dimension]) - { - return element['offset'+dimension]; - } - else - { - return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); - } - } - - var width = this.width = computeDimension(context.canvas,'Width'); - var height = this.height = computeDimension(context.canvas,'Height'); - - // Firefox requires this to work correctly - context.canvas.width = width; - context.canvas.height = height; - - var width = this.width = context.canvas.width; - var height = this.height = context.canvas.height; - this.aspectRatio = this.width / this.height; - //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - helpers.retinaScale(this); - - return this; - }; - //Globally expose the defaults to allow for user updating/changing - Chart.defaults = { - global: { - // Boolean - Whether to animate the chart - animation: true, - - // Number - Number of animation steps - animationSteps: 60, - - // String - Animation easing effect - animationEasing: "easeOutQuart", - - // Boolean - If we should show the scale at all - showScale: true, - - // Boolean - If we want to override with a hard coded scale - scaleOverride: false, - - // ** Required if scaleOverride is true ** - // Number - The number of steps in a hard coded scale - scaleSteps: null, - // Number - The value jump in the hard coded scale - scaleStepWidth: null, - // Number - The scale starting value - scaleStartValue: null, - - // String - Colour of the scale line - scaleLineColor: "rgba(0,0,0,.1)", - - // Number - Pixel width of the scale line - scaleLineWidth: 1, - - // Boolean - Whether to show labels on the scale - scaleShowLabels: true, - - // Interpolated JS string - can access value - scaleLabel: "<%=value%>", - - // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there - scaleIntegersOnly: true, - - // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero: false, - - // String - Scale label font declaration for the scale label - scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Scale label font size in pixels - scaleFontSize: 12, - - // String - Scale label font weight style - scaleFontStyle: "normal", - - // String - Scale label font colour - scaleFontColor: "#666", - - // Boolean - whether or not the chart should be responsive and resize when the browser does. - responsive: false, - - // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container - maintainAspectRatio: true, - - // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove - showTooltips: true, - - // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function - customTooltips: false, - - // Array - Array of string names to attach tooltip events - tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], - - // String - Tooltip background colour - tooltipFillColor: "rgba(0,0,0,0.8)", - - // String - Tooltip label font declaration for the scale label - tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip label font size in pixels - tooltipFontSize: 14, - - // String - Tooltip font weight style - tooltipFontStyle: "normal", - - // String - Tooltip label font colour - tooltipFontColor: "#fff", - - // String - Tooltip title font declaration for the scale label - tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip title font size in pixels - tooltipTitleFontSize: 14, - - // String - Tooltip title font weight style - tooltipTitleFontStyle: "bold", - - // String - Tooltip title font colour - tooltipTitleFontColor: "#fff", - - // Number - pixel width of padding around tooltip text - tooltipYPadding: 6, - - // Number - pixel width of padding around tooltip text - tooltipXPadding: 6, - - // Number - Size of the caret on the tooltip - tooltipCaretSize: 8, - - // Number - Pixel radius of the tooltip border - tooltipCornerRadius: 6, - - // Number - Pixel offset from point x to tooltip edge - tooltipXOffset: 10, - - // String - Template string for single tooltips - tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", - - // String - Template string for single tooltips - multiTooltipTemplate: "<%= value %>", - - // String - Colour behind the legend colour block - multiTooltipKeyBackground: '#fff', - - // Function - Will fire on animation progression. - onAnimationProgress: function(){}, - - // Function - Will fire on animation completion. - onAnimationComplete: function(){} - - } - }; - - //Create a dictionary of chart types, to allow for extension of existing types - Chart.types = {}; - - //Global Chart helpers object for utility methods and classes - var helpers = Chart.helpers = {}; - - //-- Basic js utility methods - var each = helpers.each = function(loopable,callback,self){ - var additionalArgs = Array.prototype.slice.call(arguments, 3); - // Check to see if null or undefined firstly. - if (loopable){ - if (loopable.length === +loopable.length){ - var i; - for (i=0; i= 0; i--) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)){ - return currentItem; - } - } - }, - inherits = helpers.inherits = function(extensions){ - //Basic javascript inheritance based on the model created in Backbone.js - var parent = this; - var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; - - var Surrogate = function(){ this.constructor = ChartElement;}; - Surrogate.prototype = parent.prototype; - ChartElement.prototype = new Surrogate(); - - ChartElement.extend = inherits; - - if (extensions) extend(ChartElement.prototype, extensions); - - ChartElement.__super__ = parent.prototype; - - return ChartElement; - }, - noop = helpers.noop = function(){}, - uid = helpers.uid = (function(){ - var id=0; - return function(){ - return "chart-" + id++; - }; - })(), - warn = helpers.warn = function(str){ - //Method for warning of errors - if (window.console && typeof window.console.warn == "function") console.warn(str); - }, - amd = helpers.amd = (typeof define == 'function' && define.amd), - //-- Math methods - isNumber = helpers.isNumber = function(n){ - return !isNaN(parseFloat(n)) && isFinite(n); - }, - max = helpers.max = function(array){ - return Math.max.apply( Math, array ); - }, - min = helpers.min = function(array){ - return Math.min.apply( Math, array ); - }, - cap = helpers.cap = function(valueToCap,maxValue,minValue){ - if(isNumber(maxValue)) { - if( valueToCap > maxValue ) { - return maxValue; - } - } - else if(isNumber(minValue)){ - if ( valueToCap < minValue ){ - return minValue; - } - } - return valueToCap; - }, - getDecimalPlaces = helpers.getDecimalPlaces = function(num){ - if (num%1!==0 && isNumber(num)){ - return num.toString().split(".")[1].length; - } - else { - return 0; - } - }, - toRadians = helpers.radians = function(degrees){ - return degrees * (Math.PI/180); - }, - // Gets the angle from vertical upright to the point about a centre. - getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ - var distanceFromXCenter = anglePoint.x - centrePoint.x, - distanceFromYCenter = anglePoint.y - centrePoint.y, - radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); - - - var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); - - //If the segment is in the top left quadrant, we need to add another rotation to the angle - if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ - angle += Math.PI*2; - } - - return { - angle: angle, - distance: radialDistanceFromCenter - }; - }, - aliasPixel = helpers.aliasPixel = function(pixelWidth){ - return (pixelWidth % 2 === 0) ? 0 : 0.5; - }, - splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ - //Props to Rob Spencer at scaled innovation for his post on splining between points - //http://scaledinnovation.com/analytics/splines/aboutSplines.html - var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), - d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), - fa=t*d01/(d01+d12),// scaling factor for triangle Ta - fb=t*d12/(d01+d12); - return { - inner : { - x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) - }, - outer : { - x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) - } - }; - }, - calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ - return Math.floor(Math.log(val) / Math.LN10); - }, - calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ - - //Set a minimum step of two - a point at the top of the graph, and a point at the base - var minSteps = 2, - maxSteps = Math.floor(drawingSize/(textSize * 1.5)), - skipFitting = (minSteps >= maxSteps); - - var maxValue = max(valuesArray), - minValue = min(valuesArray); - - // We need some degree of seperation here to calculate the scales if all the values are the same - // Adding/minusing 0.5 will give us a range of 1. - if (maxValue === minValue){ - maxValue += 0.5; - // So we don't end up with a graph with a negative start value if we've said always start from zero - if (minValue >= 0.5 && !startFromZero){ - minValue -= 0.5; - } - else{ - // Make up a whole number above the values - maxValue += 0.5; - } - } - - var valueRange = Math.abs(maxValue - minValue), - rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), - graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphRange = graphMax - graphMin, - stepValue = Math.pow(10, rangeOrderOfMagnitude), - numberOfSteps = Math.round(graphRange / stepValue); - - //If we have more space on the graph we'll use it to give more definition to the data - while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { - if(numberOfSteps > maxSteps){ - stepValue *=2; - numberOfSteps = Math.round(graphRange/stepValue); - // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. - if (numberOfSteps % 1 !== 0){ - skipFitting = true; - } - } - //We can fit in double the amount of scale points on the scale - else{ - //If user has declared ints only, and the step value isn't a decimal - if (integersOnly && rangeOrderOfMagnitude >= 0){ - //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float - if(stepValue/2 % 1 === 0){ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - //If it would make it a float break out of the loop - else{ - break; - } - } - //If the scale doesn't have to be an int, make the scale more granular anyway. - else{ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - - } - } - - if (skipFitting){ - numberOfSteps = minSteps; - stepValue = graphRange / numberOfSteps; - } - - return { - steps : numberOfSteps, - stepValue : stepValue, - min : graphMin, - max : graphMin + (numberOfSteps * stepValue) - }; - - }, - /* jshint ignore:start */ - // Blows up jshint errors based on the new Function constructor - //Templating methods - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - template = helpers.template = function(templateString, valuesObject){ - - // If templateString is function rather than string-template - call the function for valuesObject - - if(templateString instanceof Function){ - return templateString(valuesObject); - } - - var cache = {}; - function tmpl(str, data){ - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] : - - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") + - "');}return p.join('');" - ); - - // Provide some basic currying to the user - return data ? fn( data ) : fn; - } - return tmpl(templateString,valuesObject); - }, - /* jshint ignore:end */ - generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ - var labelsArray = new Array(numberOfSteps); - if (labelTemplateString){ - each(labelsArray,function(val,index){ - labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); - }); - } - return labelsArray; - }, - //--Animation methods - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - easingEffects = helpers.easingEffects = { - linear: function (t) { - return t; - }, - easeInQuad: function (t) { - return t * t; - }, - easeOutQuad: function (t) { - return -1 * t * (t - 2); - }, - easeInOutQuad: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; - return -1 / 2 * ((--t) * (t - 2) - 1); - }, - easeInCubic: function (t) { - return t * t * t; - }, - easeOutCubic: function (t) { - return 1 * ((t = t / 1 - 1) * t * t + 1); - }, - easeInOutCubic: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; - return 1 / 2 * ((t -= 2) * t * t + 2); - }, - easeInQuart: function (t) { - return t * t * t * t; - }, - easeOutQuart: function (t) { - return -1 * ((t = t / 1 - 1) * t * t * t - 1); - }, - easeInOutQuart: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; - return -1 / 2 * ((t -= 2) * t * t * t - 2); - }, - easeInQuint: function (t) { - return 1 * (t /= 1) * t * t * t * t; - }, - easeOutQuint: function (t) { - return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); - }, - easeInOutQuint: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; - return 1 / 2 * ((t -= 2) * t * t * t * t + 2); - }, - easeInSine: function (t) { - return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; - }, - easeOutSine: function (t) { - return 1 * Math.sin(t / 1 * (Math.PI / 2)); - }, - easeInOutSine: function (t) { - return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); - }, - easeInExpo: function (t) { - return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); - }, - easeOutExpo: function (t) { - return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); - }, - easeInOutExpo: function (t) { - if (t === 0) return 0; - if (t === 1) return 1; - if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); - return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function (t) { - if (t >= 1) return t; - return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); - }, - easeOutCirc: function (t) { - return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); - }, - easeInOutCirc: function (t) { - if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); - return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); - }, - easeInElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) return 0; - if ((t /= 1) == 1) return 1; - if (!p) p = 1 * 0.3; - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); - return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - }, - easeOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) return 0; - if ((t /= 1) == 1) return 1; - if (!p) p = 1 * 0.3; - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); - return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; - }, - easeInOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) return 0; - if ((t /= 1 / 2) == 2) return 1; - if (!p) p = 1 * (0.3 * 1.5); - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); - if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; - }, - easeInBack: function (t) { - var s = 1.70158; - return 1 * (t /= 1) * t * ((s + 1) * t - s); - }, - easeOutBack: function (t) { - var s = 1.70158; - return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); - }, - easeInOutBack: function (t) { - var s = 1.70158; - if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); - return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); - }, - easeInBounce: function (t) { - return 1 - easingEffects.easeOutBounce(1 - t); - }, - easeOutBounce: function (t) { - if ((t /= 1) < (1 / 2.75)) { - return 1 * (7.5625 * t * t); - } else if (t < (2 / 2.75)) { - return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); - } else if (t < (2.5 / 2.75)) { - return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); - } else { - return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); - } - }, - easeInOutBounce: function (t) { - if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; - return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; - } - }, - //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ - requestAnimFrame = helpers.requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - return window.setTimeout(callback, 1000 / 60); - }; - })(), - cancelAnimFrame = helpers.cancelAnimFrame = (function(){ - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - })(), - animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ - - var currentStep = 0, - easingFunction = easingEffects[easingString] || easingEffects.linear; - - var animationFrame = function(){ - currentStep++; - var stepDecimal = currentStep/totalSteps; - var easeDecimal = easingFunction(stepDecimal); - - callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); - onProgress.call(chartInstance,easeDecimal,stepDecimal); - if (currentStep < totalSteps){ - chartInstance.animationFrame = requestAnimFrame(animationFrame); - } else{ - onComplete.apply(chartInstance); - } - }; - requestAnimFrame(animationFrame); - }, - //-- DOM methods - getRelativePosition = helpers.getRelativePosition = function(evt){ - var mouseX, mouseY; - var e = evt.originalEvent || evt, - canvas = evt.currentTarget || evt.srcElement, - boundingRect = canvas.getBoundingClientRect(); - - if (e.touches){ - mouseX = e.touches[0].clientX - boundingRect.left; - mouseY = e.touches[0].clientY - boundingRect.top; - - } - else{ - mouseX = e.clientX - boundingRect.left; - mouseY = e.clientY - boundingRect.top; - } - - return { - x : mouseX, - y : mouseY - }; - - }, - addEvent = helpers.addEvent = function(node,eventType,method){ - if (node.addEventListener){ - node.addEventListener(eventType,method); - } else if (node.attachEvent){ - node.attachEvent("on"+eventType, method); - } else { - node["on"+eventType] = method; - } - }, - removeEvent = helpers.removeEvent = function(node, eventType, handler){ - if (node.removeEventListener){ - node.removeEventListener(eventType, handler, false); - } else if (node.detachEvent){ - node.detachEvent("on"+eventType,handler); - } else{ - node["on" + eventType] = noop; - } - }, - bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ - // Create the events object if it's not already present - if (!chartInstance.events) chartInstance.events = {}; - - each(arrayOfEvents,function(eventName){ - chartInstance.events[eventName] = function(){ - handler.apply(chartInstance, arguments); - }; - addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); - }); - }, - unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { - each(arrayOfEvents, function(handler,eventName){ - removeEvent(chartInstance.chart.canvas, eventName, handler); - }); - }, - getMaximumWidth = helpers.getMaximumWidth = function(domNode){ - var container = domNode.parentNode; - // TODO = check cross browser stuff with this. - return container.clientWidth; - }, - getMaximumHeight = helpers.getMaximumHeight = function(domNode){ - var container = domNode.parentNode; - // TODO = check cross browser stuff with this. - return container.clientHeight; - }, - getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support - retinaScale = helpers.retinaScale = function(chart){ - var ctx = chart.ctx, - width = chart.canvas.width, - height = chart.canvas.height; - - if (window.devicePixelRatio) { - ctx.canvas.style.width = width + "px"; - ctx.canvas.style.height = height + "px"; - ctx.canvas.height = height * window.devicePixelRatio; - ctx.canvas.width = width * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - }, - //-- Canvas methods - clear = helpers.clear = function(chart){ - chart.ctx.clearRect(0,0,chart.width,chart.height); - }, - fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ - return fontStyle + " " + pixelSize+"px " + fontFamily; - }, - longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ - ctx.font = font; - var longest = 0; - each(arrayOfStrings,function(string){ - var textWidth = ctx.measureText(string).width; - longest = (textWidth > longest) ? textWidth : longest; - }); - return longest; - }, - drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - }; - - - //Store a reference to each instance - allowing us to globally resize chart instances on window resize. - //Destroy method on the chart will remove the instance of the chart from this reference. - Chart.instances = {}; - - Chart.Type = function(data,options,chart){ - this.options = options; - this.chart = chart; - this.id = uid(); - //Add the chart instance to the global namespace - Chart.instances[this.id] = this; - - // Initialize is always called when a chart type is created - // By default it is a no op, but it should be extended - if (options.responsive){ - this.resize(); - } - this.initialize.call(this,data); - }; - - //Core methods that'll be a part of every chart type - extend(Chart.Type.prototype,{ - initialize : function(){return this;}, - clear : function(){ - clear(this.chart); - return this; - }, - stop : function(){ - // Stops any current animation loop occuring - cancelAnimFrame(this.animationFrame); - return this; - }, - resize : function(callback){ - this.stop(); - var canvas = this.chart.canvas, - newWidth = getMaximumWidth(this.chart.canvas), - newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); - - canvas.width = this.chart.width = newWidth; - canvas.height = this.chart.height = newHeight; - - retinaScale(this.chart); - - if (typeof callback === "function"){ - callback.apply(this, Array.prototype.slice.call(arguments, 1)); - } - return this; - }, - reflow : noop, - render : function(reflow){ - if (reflow){ - this.reflow(); - } - if (this.options.animation && !reflow){ - helpers.animationLoop( - this.draw, - this.options.animationSteps, - this.options.animationEasing, - this.options.onAnimationProgress, - this.options.onAnimationComplete, - this - ); - } - else{ - this.draw(); - this.options.onAnimationComplete.call(this); - } - return this; - }, - generateLegend : function(){ - return template(this.options.legendTemplate,this); - }, - destroy : function(){ - this.clear(); - unbindEvents(this, this.events); - var canvas = this.chart.canvas; - - // Reset canvas height/width attributes starts a fresh with the canvas context - canvas.width = this.chart.width; - canvas.height = this.chart.height; - - // < IE9 doesn't support removeProperty - if (canvas.style.removeProperty) { - canvas.style.removeProperty('width'); - canvas.style.removeProperty('height'); - } else { - canvas.style.removeAttribute('width'); - canvas.style.removeAttribute('height'); - } - - delete Chart.instances[this.id]; - }, - showTooltip : function(ChartElements, forceRedraw){ - // Only redraw the chart if we've actually changed what we're hovering on. - if (typeof this.activeElements === 'undefined') this.activeElements = []; - - var isChanged = (function(Elements){ - var changed = false; - - if (Elements.length !== this.activeElements.length){ - changed = true; - return changed; - } - - each(Elements, function(element, index){ - if (element !== this.activeElements[index]){ - changed = true; - } - }, this); - return changed; - }).call(this, ChartElements); - - if (!isChanged && !forceRedraw){ - return; - } - else{ - this.activeElements = ChartElements; - } - this.draw(); - if(this.options.customTooltips){ - this.options.customTooltips(false); - } - if (ChartElements.length > 0){ - // If we have multiple datasets, show a MultiTooltip for all of the data points at that index - if (this.datasets && this.datasets.length > 1) { - var dataArray, - dataIndex; - - for (var i = this.datasets.length - 1; i >= 0; i--) { - dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; - dataIndex = indexOf(dataArray, ChartElements[0]); - if (dataIndex !== -1){ - break; - } - } - var tooltipLabels = [], - tooltipColors = [], - medianPosition = (function(index) { - - // Get all the points at that particular index - var Elements = [], - dataCollection, - xPositions = [], - yPositions = [], - xMax, - yMax, - xMin, - yMin; - helpers.each(this.datasets, function(dataset){ - dataCollection = dataset.points || dataset.bars || dataset.segments; - if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ - Elements.push(dataCollection[dataIndex]); - } - }); - - helpers.each(Elements, function(element) { - xPositions.push(element.x); - yPositions.push(element.y); - - - //Include any colour information about the element - tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); - tooltipColors.push({ - fill: element._saved.fillColor || element.fillColor, - stroke: element._saved.strokeColor || element.strokeColor - }); - - }, this); - - yMin = min(yPositions); - yMax = max(yPositions); - - xMin = min(xPositions); - xMax = max(xPositions); - - return { - x: (xMin > this.chart.width/2) ? xMin : xMax, - y: (yMin + yMax)/2 - }; - }).call(this, dataIndex); - - new Chart.MultiTooltip({ - x: medianPosition.x, - y: medianPosition.y, - xPadding: this.options.tooltipXPadding, - yPadding: this.options.tooltipYPadding, - xOffset: this.options.tooltipXOffset, - fillColor: this.options.tooltipFillColor, - textColor: this.options.tooltipFontColor, - fontFamily: this.options.tooltipFontFamily, - fontStyle: this.options.tooltipFontStyle, - fontSize: this.options.tooltipFontSize, - titleTextColor: this.options.tooltipTitleFontColor, - titleFontFamily: this.options.tooltipTitleFontFamily, - titleFontStyle: this.options.tooltipTitleFontStyle, - titleFontSize: this.options.tooltipTitleFontSize, - cornerRadius: this.options.tooltipCornerRadius, - labels: tooltipLabels, - legendColors: tooltipColors, - legendColorBackground : this.options.multiTooltipKeyBackground, - title: ChartElements[0].label, - chart: this.chart, - ctx: this.chart.ctx, - custom: this.options.customTooltips - }).draw(); - - } else { - each(ChartElements, function(Element) { - var tooltipPosition = Element.tooltipPosition(); - new Chart.Tooltip({ - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - xPadding: this.options.tooltipXPadding, - yPadding: this.options.tooltipYPadding, - fillColor: this.options.tooltipFillColor, - textColor: this.options.tooltipFontColor, - fontFamily: this.options.tooltipFontFamily, - fontStyle: this.options.tooltipFontStyle, - fontSize: this.options.tooltipFontSize, - caretHeight: this.options.tooltipCaretSize, - cornerRadius: this.options.tooltipCornerRadius, - text: template(this.options.tooltipTemplate, Element), - chart: this.chart, - custom: this.options.customTooltips - }).draw(); - }, this); - } - } - return this; - }, - toBase64Image : function(){ - return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); - } - }); - - Chart.Type.extend = function(extensions){ - - var parent = this; - - var ChartType = function(){ - return parent.apply(this,arguments); - }; - - //Copy the prototype object of the this class - ChartType.prototype = clone(parent.prototype); - //Now overwrite some of the properties in the base class with the new extensions - extend(ChartType.prototype, extensions); - - ChartType.extend = Chart.Type.extend; - - if (extensions.name || parent.prototype.name){ - - var chartName = extensions.name || parent.prototype.name; - //Assign any potential default values of the new chart type - - //If none are defined, we'll use a clone of the chart type this is being extended from. - //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart - //doesn't define some defaults of their own. - - var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; - - Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); - - Chart.types[chartName] = ChartType; - - //Register this new chart type in the Chart prototype - Chart.prototype[chartName] = function(data,options){ - var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); - return new ChartType(data,config,this); - }; - } else{ - warn("Name not provided for this chart, so it hasn't been registered"); - } - return parent; - }; - - Chart.Element = function(configuration){ - extend(this,configuration); - this.initialize.apply(this,arguments); - this.save(); - }; - extend(Chart.Element.prototype,{ - initialize : function(){}, - restore : function(props){ - if (!props){ - extend(this,this._saved); - } else { - each(props,function(key){ - this[key] = this._saved[key]; - },this); - } - return this; - }, - save : function(){ - this._saved = clone(this); - delete this._saved._saved; - return this; - }, - update : function(newProps){ - each(newProps,function(value,key){ - this._saved[key] = this[key]; - this[key] = value; - },this); - return this; - }, - transition : function(props,ease){ - each(props,function(value,key){ - this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; - },this); - return this; - }, - tooltipPosition : function(){ - return { - x : this.x, - y : this.y - }; - }, - hasValue: function(){ - return isNumber(this.value); - } - }); - - Chart.Element.extend = inherits; - - - Chart.Point = Chart.Element.extend({ - display: true, - inRange: function(chartX,chartY){ - var hitDetectionRange = this.hitDetectionRadius + this.radius; - return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); - }, - draw : function(){ - if (this.display){ - var ctx = this.ctx; - ctx.beginPath(); - - ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); - ctx.closePath(); - - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - ctx.fillStyle = this.fillColor; - - ctx.fill(); - ctx.stroke(); - } - - - //Quick debug for bezier curve splining - //Highlights control points and the line between them. - //Handy for dev - stripped in the min version. - - // ctx.save(); - // ctx.fillStyle = "black"; - // ctx.strokeStyle = "black" - // ctx.beginPath(); - // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); - // ctx.fill(); - - // ctx.beginPath(); - // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); - // ctx.fill(); - - // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); - // ctx.lineTo(this.x, this.y); - // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); - // ctx.stroke(); - - // ctx.restore(); - - - - } - }); - - Chart.Arc = Chart.Element.extend({ - inRange : function(chartX,chartY){ - - var pointRelativePosition = helpers.getAngleFromPoint(this, { - x: chartX, - y: chartY - }); - - //Check if within the range of the open/close angle - var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), - withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); - - return (betweenAngles && withinRadius); - //Ensure within the outside of the arc centre, but inside arc outer - }, - tooltipPosition : function(){ - var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), - rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; - return { - x : this.x + (Math.cos(centreAngle) * rangeFromCentre), - y : this.y + (Math.sin(centreAngle) * rangeFromCentre) - }; - }, - draw : function(animationPercent){ - - var easingDecimal = animationPercent || 1; - - var ctx = this.ctx; - - ctx.beginPath(); - - ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); - - ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); - - ctx.closePath(); - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - ctx.fillStyle = this.fillColor; - - ctx.fill(); - ctx.lineJoin = 'bevel'; - - if (this.showStroke){ - ctx.stroke(); - } - } - }); - - Chart.Rectangle = Chart.Element.extend({ - draw : function(){ - var ctx = this.ctx, - halfWidth = this.width/2, - leftX = this.x - halfWidth, - rightX = this.x + halfWidth, - top = this.base - (this.base - this.y), - halfStroke = this.strokeWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (this.showStroke){ - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = this.fillColor; - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - // It'd be nice to keep this class totally generic to any rectangle - // and simply specify which border to miss out. - ctx.moveTo(leftX, this.base); - ctx.lineTo(leftX, top); - ctx.lineTo(rightX, top); - ctx.lineTo(rightX, this.base); - ctx.fill(); - if (this.showStroke){ - ctx.stroke(); - } - }, - height : function(){ - return this.base - this.y; - }, - inRange : function(chartX,chartY){ - return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); - } - }); - - Chart.Tooltip = Chart.Element.extend({ - draw : function(){ - - var ctx = this.chart.ctx; - - ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); - - this.xAlign = "center"; - this.yAlign = "above"; - - //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = this.caretPadding = 2; - - var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, - tooltipRectHeight = this.fontSize + 2*this.yPadding, - tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; - - if (this.x + tooltipWidth/2 >this.chart.width){ - this.xAlign = "left"; - } else if (this.x - tooltipWidth/2 < 0){ - this.xAlign = "right"; - } - - if (this.y - tooltipHeight < 0){ - this.yAlign = "below"; - } - - - var tooltipX = this.x - tooltipWidth/2, - tooltipY = this.y - tooltipHeight; - - ctx.fillStyle = this.fillColor; - - // Custom Tooltips - if(this.custom){ - this.custom(this); - } - else{ - switch(this.yAlign) - { - case "above": - //Draw a caret above the x/y - ctx.beginPath(); - ctx.moveTo(this.x,this.y - caretPadding); - ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); - ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); - ctx.closePath(); - ctx.fill(); - break; - case "below": - tooltipY = this.y + caretPadding + this.caretHeight; - //Draw a caret below the x/y - ctx.beginPath(); - ctx.moveTo(this.x, this.y + caretPadding); - ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); - ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); - ctx.closePath(); - ctx.fill(); - break; - } - - switch(this.xAlign) - { - case "left": - tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); - break; - case "right": - tooltipX = this.x - (this.cornerRadius + this.caretHeight); - break; - } - - drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); - - ctx.fill(); - - ctx.fillStyle = this.textColor; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); - } - } - }); - - Chart.MultiTooltip = Chart.Element.extend({ - initialize : function(){ - this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); - - this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); - - this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; - - this.ctx.font = this.titleFont; - - var titleWidth = this.ctx.measureText(this.title).width, - //Label has a legend square as well so account for this. - labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, - longestTextWidth = max([labelWidth,titleWidth]); - - this.width = longestTextWidth + (this.xPadding*2); - - - var halfHeight = this.height/2; - - //Check to ensure the height will fit on the canvas - if (this.y - halfHeight < 0 ){ - this.y = halfHeight; - } else if (this.y + halfHeight > this.chart.height){ - this.y = this.chart.height - halfHeight; - } - - //Decide whether to align left or right based on position on canvas - if (this.x > this.chart.width/2){ - this.x -= this.xOffset + this.width; - } else { - this.x += this.xOffset; - } - - - }, - getLineHeight : function(index){ - var baseLineHeight = this.y - (this.height/2) + this.yPadding, - afterTitleIndex = index-1; - - //If the index is zero, we're getting the title - if (index === 0){ - return baseLineHeight + this.titleFontSize/2; - } else{ - return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; - } - - }, - draw : function(){ - // Custom Tooltips - if(this.custom){ - this.custom(this); - } - else{ - drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); - var ctx = this.ctx; - ctx.fillStyle = this.fillColor; - ctx.fill(); - ctx.closePath(); - - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.titleTextColor; - ctx.font = this.titleFont; - - ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); - - ctx.font = this.font; - helpers.each(this.labels,function(label,index){ - ctx.fillStyle = this.textColor; - ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); - - //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) - //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - //Instead we'll make a white filled block to put the legendColour palette over. - - ctx.fillStyle = this.legendColorBackground; - ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - - ctx.fillStyle = this.legendColors[index].fill; - ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - - - },this); - } - } - }); - - Chart.Scale = Chart.Element.extend({ - initialize : function(){ - this.fit(); - }, - buildYLabels : function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; - }, - addXLabel : function(label){ - this.xLabels.push(label); - this.valuesCount++; - this.fit(); - }, - removeXLabel : function(){ - this.xLabels.shift(); - this.valuesCount--; - this.fit(); - }, - // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use - fit: function(){ - // First we need the width of the yLabels, assuming the xLabels aren't rotated - - // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation - this.startPoint = (this.display) ? this.fontSize : 0; - this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels - - // Apply padding settings to the start and end point. - this.startPoint += this.padding; - this.endPoint -= this.padding; - - // Cache the starting height, so can determine if we need to recalculate the scale yAxis - var cachedHeight = this.endPoint - this.startPoint, - cachedYLabelWidth; - - // Build the current yLabels so we have an idea of what size they'll be to start - /* - * This sets what is returned from calculateScaleRange as static properties of this class: - * - this.steps; - this.stepValue; - this.min; - this.max; - * - */ - this.calculateYRange(cachedHeight); - - // With these properties set we can now build the array of yLabels - // and also the width of the largest yLabel - this.buildYLabels(); - - this.calculateXLabelRotation(); - - while((cachedHeight > this.endPoint - this.startPoint)){ - cachedHeight = this.endPoint - this.startPoint; - cachedYLabelWidth = this.yLabelWidth; - - this.calculateYRange(cachedHeight); - this.buildYLabels(); - - // Only go through the xLabel loop again if the yLabel width has changed - if (cachedYLabelWidth < this.yLabelWidth){ - this.calculateXLabelRotation(); - } - } - - }, - calculateXLabelRotation : function(){ - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - - this.ctx.font = this.font; - - var firstWidth = this.ctx.measureText(this.xLabels[0]).width, - lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, - firstRotated, - lastRotated; - - - this.xScalePaddingRight = lastWidth/2 + 3; - this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; - - this.xLabelRotation = 0; - if (this.display){ - var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), - cosRotation, - firstRotatedWidth; - this.xLabelWidth = originalLabelWidth; - //Allow 3 pixels x2 padding either side for label readability - var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; - - //Max label rotate should be 90 - also act as a loop counter - while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ - cosRotation = Math.cos(toRadians(this.xLabelRotation)); - - firstRotated = cosRotation * firstWidth; - lastRotated = cosRotation * lastWidth; - - // We're right aligning the text now. - if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ - this.xScalePaddingLeft = firstRotated + this.fontSize / 2; - } - this.xScalePaddingRight = this.fontSize/2; - - - this.xLabelRotation++; - this.xLabelWidth = cosRotation * originalLabelWidth; - - } - if (this.xLabelRotation > 0){ - this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; - } - } - else{ - this.xLabelWidth = 0; - this.xScalePaddingRight = this.padding; - this.xScalePaddingLeft = this.padding; - } - - }, - // Needs to be overidden in each Chart type - // Otherwise we need to pass all the data into the scale class - calculateYRange: noop, - drawingArea: function(){ - return this.startPoint - this.endPoint; - }, - calculateY : function(value){ - var scalingFactor = this.drawingArea() / (this.min - this.max); - return this.endPoint - (scalingFactor * (value - this.min)); - }, - calculateX : function(index){ - var isRotated = (this.xLabelRotation > 0), - // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, - innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), - valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), - valueOffset = (valueWidth * index) + this.xScalePaddingLeft; - - if (this.offsetGridLines){ - valueOffset += (valueWidth/2); - } - - return Math.round(valueOffset); - }, - update : function(newProps){ - helpers.extend(this, newProps); - this.fit(); - }, - draw : function(){ - var ctx = this.ctx, - yLabelGap = (this.endPoint - this.startPoint) / this.steps, - xStart = Math.round(this.xScalePaddingLeft); - if (this.display){ - ctx.fillStyle = this.textColor; - ctx.font = this.font; - each(this.yLabels,function(labelString,index){ - var yLabelCenter = this.endPoint - (yLabelGap * index), - linePositionY = Math.round(yLabelCenter), - drawHorizontalLine = this.showHorizontalLines; - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - if (this.showLabels){ - ctx.fillText(labelString,xStart - 10,yLabelCenter); - } - - // This is X axis, so draw it - if (index === 0 && !drawHorizontalLine){ - drawHorizontalLine = true; - } - - if (drawHorizontalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - linePositionY += helpers.aliasPixel(ctx.lineWidth); - - if(drawHorizontalLine){ - ctx.moveTo(xStart, linePositionY); - ctx.lineTo(this.width, linePositionY); - ctx.stroke(); - ctx.closePath(); - } - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - ctx.beginPath(); - ctx.moveTo(xStart - 5, linePositionY); - ctx.lineTo(xStart, linePositionY); - ctx.stroke(); - ctx.closePath(); - - },this); - - each(this.xLabels,function(label,index){ - var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), - // Check to see if line/bar here and decide where to place the line - linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), - isRotated = (this.xLabelRotation > 0), - drawVerticalLine = this.showVerticalLines; - - // This is Y axis, so draw it - if (index === 0 && !drawVerticalLine){ - drawVerticalLine = true; - } - - if (drawVerticalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - if (drawVerticalLine){ - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.startPoint - 3); - ctx.stroke(); - ctx.closePath(); - } - - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - - - // Small lines at the bottom of the base grid line - ctx.beginPath(); - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.endPoint + 5); - ctx.stroke(); - ctx.closePath(); - - ctx.save(); - ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); - ctx.rotate(toRadians(this.xLabelRotation)*-1); - ctx.font = this.font; - ctx.textAlign = (isRotated) ? "right" : "center"; - ctx.textBaseline = (isRotated) ? "middle" : "top"; - ctx.fillText(label, 0, 0); - ctx.restore(); - },this); - - } - } - - }); - - Chart.RadialScale = Chart.Element.extend({ - initialize: function(){ - this.size = min([this.height, this.width]); - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - }, - calculateCenterOffset: function(value){ - // Take into account half font size + the yPadding of the top value - var scalingFactor = this.drawingArea / (this.max - this.min); - - return (value - this.min) * scalingFactor; - }, - update : function(){ - if (!this.lineArc){ - this.setScaleSize(); - } else { - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - } - this.buildYLabels(); - }, - buildYLabels: function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - }, - getCircumference : function(){ - return ((Math.PI*2) / this.valuesCount); - }, - setScaleSize: function(){ - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft, - maxWidthRadius; - this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - for (i=0;i furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } - else if (i < this.valuesCount/2) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - } - else if (i > this.valuesCount/2){ - // More than half the values means we'll right align the text - if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } - } - } - - xProtrusionLeft = furthestLeft; - - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); - - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; - - //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); - - }, - setCenterPoint: function(leftMovement, rightMovement){ - - var maxRight = this.width - rightMovement - this.drawingArea, - maxLeft = leftMovement + this.drawingArea; - - this.xCenter = (maxLeft + maxRight)/2; - // Always vertically in the centre as the text height doesn't change - this.yCenter = (this.height/2); - }, - - getIndexAngle : function(index){ - var angleMultiplier = (Math.PI * 2) / this.valuesCount; - // Start from the top instead of right, so remove a quarter of the circle - - return index * angleMultiplier - (Math.PI/2); - }, - getPointPosition : function(index, distanceFromCenter){ - var thisAngle = this.getIndexAngle(index); - return { - x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, - y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter - }; - }, - draw: function(){ - if (this.display){ - var ctx = this.ctx; - each(this.yLabels, function(label, index){ - // Don't draw a centre value - if (index > 0){ - var yCenterOffset = index * (this.drawingArea/this.steps), - yHeight = this.yCenter - yCenterOffset, - pointPosition; - - // Draw circular lines around the scale - if (this.lineWidth > 0){ - ctx.strokeStyle = this.lineColor; - ctx.lineWidth = this.lineWidth; - - if(this.lineArc){ - ctx.beginPath(); - ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); - ctx.closePath(); - ctx.stroke(); - } else{ - ctx.beginPath(); - for (var i=0;i= 0; i--) { - if (this.angleLineWidth > 0){ - var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); - ctx.beginPath(); - ctx.moveTo(this.xCenter, this.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); - ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - ctx.fillStyle = this.pointLabelFontColor; - - var labelsCount = this.labels.length, - halfLabelsCount = this.labels.length/2, - quarterLabelsCount = halfLabelsCount/2, - upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), - exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); - if (i === 0){ - ctx.textAlign = 'center'; - } else if(i === halfLabelsCount){ - ctx.textAlign = 'center'; - } else if (i < halfLabelsCount){ - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (exactQuarter){ - ctx.textBaseline = 'middle'; - } else if (upperHalf){ - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); - } - } - } - } - }); - - // Attach global event to resize each chart instance when the browser resizes - helpers.addEvent(window, "resize", (function(){ - // Basic debounce of resize function so it doesn't hurt performance when resizing browser. - var timeout; - return function(){ - clearTimeout(timeout); - timeout = setTimeout(function(){ - each(Chart.instances,function(instance){ - // If the responsive flag is set in the chart instance config - // Cascade the resize event down to the chart. - if (instance.options.responsive){ - instance.resize(instance.render, true); - } - }); - }, 50); - }; - })()); - - - if (amd) { - define(function(){ - return Chart; - }); - } else if (typeof module === 'object' && module.exports) { - module.exports = Chart; - } - - root.Chart = Chart; - - Chart.noConflict = function(){ - root.Chart = previous; - return Chart; - }; - -}).call(this); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - var defaultConfig = { - //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero : true, - - //Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - If there is a stroke on each bar - barShowStroke : true, - - //Number - Pixel width of the bar stroke - barStrokeWidth : 2, - - //Number - Spacing between each of the X value sets - barValueSpacing : 5, - - //Number - Spacing between data sets within X values - barDatasetSpacing : 1, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - name: "Bar", - defaults : defaultConfig, - initialize: function(data){ - - //Expose options as a scope variable here so we can access it in the ScaleClass - var options = this.options; - - this.ScaleClass = Chart.Scale.extend({ - offsetGridLines : true, - calculateBarX : function(datasetCount, datasetIndex, barIndex){ - //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.calculateX(barIndex) - (xWidth/2), - barWidth = this.calculateBarWidth(datasetCount); - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; - }, - calculateBaseWidth : function(){ - return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); - }, - calculateBarWidth : function(datasetCount){ - //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset - var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); - - return (baseWidth / datasetCount); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; - - this.eachBars(function(bar){ - bar.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activeBars, function(activeBar){ - activeBar.fillColor = activeBar.highlightFill; - activeBar.strokeColor = activeBar.highlightStroke; - }); - this.showTooltip(activeBars); - }); - } - - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.BarClass = Chart.Rectangle.extend({ - strokeWidth : this.options.barStrokeWidth, - showStroke : this.options.barShowStroke, - ctx : this.chart.ctx - }); - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset,datasetIndex){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - bars : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.bars.push(new this.BarClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.strokeColor, - fillColor : dataset.fillColor, - highlightFill : dataset.highlightFill || dataset.fillColor, - highlightStroke : dataset.highlightStroke || dataset.strokeColor - })); - },this); - - },this); - - this.buildScale(data.labels); - - this.BarClass.prototype.base = this.scale.endPoint; - - this.eachBars(function(bar, index, datasetIndex){ - helpers.extend(bar, { - width : this.scale.calculateBarWidth(this.datasets.length), - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y: this.scale.endPoint - }); - bar.save(); - }, this); - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - - this.eachBars(function(bar){ - bar.save(); - }); - this.render(); - }, - eachBars : function(callback){ - helpers.each(this.datasets,function(dataset, datasetIndex){ - helpers.each(dataset.bars, callback, this, datasetIndex); - },this); - }, - getBarsAtEvent : function(e){ - var barsArray = [], - eventPosition = helpers.getRelativePosition(e), - datasetIterator = function(dataset){ - barsArray.push(dataset.bars[barIndex]); - }, - barIndex; - - for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { - for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { - if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ - helpers.each(this.datasets, datasetIterator); - return barsArray; - } - } - } - - return barsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachBars(function(bar){ - values.push(bar.value); - }); - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange: function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - this.scale = new this.ScaleClass(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].bars.push(new this.BarClass({ - value : value, - label : label, - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), - y: this.scale.endPoint, - width : this.scale.calculateBarWidth(this.datasets.length), - base : this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].strokeColor, - fillColor : this.datasets[datasetIndex].fillColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.bars.shift(); - },this); - this.update(); - }, - reflow : function(){ - helpers.extend(this.BarClass.prototype,{ - y: this.scale.endPoint, - base : this.scale.endPoint - }); - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - this.scale.draw(easingDecimal); - - //Draw all the bars for each dataset - helpers.each(this.datasets,function(dataset,datasetIndex){ - helpers.each(dataset.bars,function(bar,index){ - if (bar.hasValue()){ - bar.base = this.scale.endPoint; - //Transition then draw - bar.transition({ - x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y : this.scale.calculateY(bar.value), - width : this.scale.calculateBarWidth(this.datasets.length) - }, easingDecimal).draw(); - } - },this); - - },this); - } - }); - - -}).call(this); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Whether we should show a stroke on each segment - segmentShowStroke : true, - - //String - The colour of each segment stroke - segmentStrokeColor : "#fff", - - //Number - The width of each segment stroke - segmentStrokeWidth : 2, - - //The percentage of the chart that we cut out of the middle. - percentageInnerCutout : 50, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect - animationEasing : "easeOutBounce", - - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate : true, - - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "Doughnut", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - - //Declare segments as a static property to prevent inheriting across the Chart type prototype - this.segments = []; - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - - this.SegmentArc = Chart.Arc.extend({ - ctx : this.chart.ctx, - x : this.chart.width/2, - y : this.chart.height/2 - }); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - this.calculateTotal(data); - - helpers.each(data,function(datapoint, index){ - this.addData(datapoint, index, true); - },this); - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - this.segments.splice(index, 0, new this.SegmentArc({ - value : segment.value, - outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, - innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, - fillColor : segment.color, - highlightColor : segment.highlight || segment.color, - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - startAngle : Math.PI * 1.5, - circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), - label : segment.label - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - calculateCircumference : function(value){ - return (Math.PI*2)*(Math.abs(value) / this.total); - }, - calculateTotal : function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += Math.abs(segment.value); - },this); - }, - update : function(){ - this.calculateTotal(this.segments); - - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor']); - }); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - this.render(); - }, - - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - }); - }, this); - }, - draw : function(easeDecimal){ - var animDecimal = (easeDecimal) ? easeDecimal : 1; - this.clear(); - helpers.each(this.segments,function(segment,index){ - segment.transition({ - circumference : this.calculateCircumference(segment.value), - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - },animDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - segment.draw(); - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length-1){ - this.segments[index+1].startAngle = segment.endAngle; - } - },this); - - } - }); - - Chart.types.Doughnut.extend({ - name : "Pie", - defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) - }); - -}).call(this); -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var defaultConfig = { - - ///Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - Whether the line is curved between points - bezierCurve : true, - - //Number - Tension of the bezier curve between points - bezierCurveTension : 0.4, - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 4, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - name: "Line", - defaults : defaultConfig, - initialize: function(data){ - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx, - inRange : function(mouseX){ - return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePoints, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - this.showTooltip(activePoints); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - this.buildScale(data.labels); - - - this.eachPoints(function(point, index){ - helpers.extend(point, { - x: this.scale.calculateX(index), - y: this.scale.endPoint - }); - point.save(); - }, this); - - },this); - - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - this.eachPoints(function(point){ - point.save(); - }); - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - getPointsAtEvent : function(e){ - var pointsArray = [], - eventPosition = helpers.getRelativePosition(e); - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,function(point){ - if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); - }); - },this); - return pointsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachPoints(function(point){ - values.push(point.value); - }); - - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange : function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - - this.scale = new Chart.Scale(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - x: this.scale.calculateX(this.scale.valuesCount+1), - y: this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.update(); - }, - reflow : function(){ - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - // Some helper methods for getting the next/prev points - var hasValue = function(item){ - return item.value !== null; - }, - nextPoint = function(point, collection, index){ - return helpers.findNextWhere(collection, hasValue, index) || point; - }, - previousPoint = function(point, collection, index){ - return helpers.findPreviousWhere(collection, hasValue, index) || point; - }; - - this.scale.draw(easingDecimal); - - - helpers.each(this.datasets,function(dataset){ - var pointsWithValues = helpers.where(dataset.points, hasValue); - - //Transition each point first so that the line and point drawing isn't out of sync - //We can use this extra loop to calculate the control points of this dataset also in this loop - - helpers.each(dataset.points, function(point, index){ - if (point.hasValue()){ - point.transition({ - y : this.scale.calculateY(point.value), - x : this.scale.calculateX(index) - }, easingDecimal); - } - },this); - - - // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point - // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed - if (this.options.bezierCurve){ - helpers.each(pointsWithValues, function(point, index){ - var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; - point.controlPoints = helpers.splineCurve( - previousPoint(point, pointsWithValues, index), - point, - nextPoint(point, pointsWithValues, index), - tension - ); - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (point.controlPoints.outer.y > this.scale.endPoint){ - point.controlPoints.outer.y = this.scale.endPoint; - } - else if (point.controlPoints.outer.y < this.scale.startPoint){ - point.controlPoints.outer.y = this.scale.startPoint; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (point.controlPoints.inner.y > this.scale.endPoint){ - point.controlPoints.inner.y = this.scale.endPoint; - } - else if (point.controlPoints.inner.y < this.scale.startPoint){ - point.controlPoints.inner.y = this.scale.startPoint; - } - },this); - } - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - - helpers.each(pointsWithValues, function(point, index){ - if (index === 0){ - ctx.moveTo(point.x, point.y); - } - else{ - if(this.options.bezierCurve){ - var previous = previousPoint(point, pointsWithValues, index); - - ctx.bezierCurveTo( - previous.controlPoints.outer.x, - previous.controlPoints.outer.y, - point.controlPoints.inner.x, - point.controlPoints.inner.y, - point.x, - point.y - ); - } - else{ - ctx.lineTo(point.x,point.y); - } - } - }, this); - - ctx.stroke(); - - if (this.options.datasetFill && pointsWithValues.length > 0){ - //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); - ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); - ctx.fillStyle = dataset.fillColor; - ctx.closePath(); - ctx.fill(); - } - - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(pointsWithValues,function(point){ - point.draw(); - }); - },this); - } - }); - - -}).call(this); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Show a backdrop to the scale label - scaleShowLabelBackdrop : true, - - //String - The colour of the label backdrop - scaleBackdropColor : "rgba(255,255,255,0.75)", - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //Number - The backdrop padding above & below the label in pixels - scaleBackdropPaddingY : 2, - - //Number - The backdrop padding to the side of the label in pixels - scaleBackdropPaddingX : 2, - - //Boolean - Show line for each value in the scale - scaleShowLine : true, - - //Boolean - Stroke a line around each segment in the chart - segmentShowStroke : true, - - //String - The colour of the stroke on each segement. - segmentStrokeColor : "#fff", - - //Number - The width of the stroke value in pixels - segmentStrokeWidth : 2, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect. - animationEasing : "easeOutBounce", - - //Boolean - Whether to animate the rotation of the chart - animateRotate : true, - - //Boolean - Whether to animate scaling the chart from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "PolarArea", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - this.segments = []; - //Declare segment class as a chart instance specific class, so it can share props for this instance - this.SegmentArc = Chart.Arc.extend({ - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - ctx : this.chart.ctx, - innerRadius : 0, - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - lineArc: true, - width: this.chart.width, - height: this.chart.height, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - valuesCount: data.length - }); - - this.updateScaleRange(data); - - this.scale.update(); - - helpers.each(data,function(segment,index){ - this.addData(segment,index,true); - },this); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - - this.segments.splice(index, 0, new this.SegmentArc({ - fillColor: segment.color, - highlightColor: segment.highlight || segment.color, - label: segment.label, - value: segment.value, - outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), - circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), - startAngle: Math.PI * 1.5 - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - calculateTotal: function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += segment.value; - },this); - this.scale.valuesCount = this.segments.length; - }, - updateScaleRange: function(datapoints){ - var valuesArray = []; - helpers.each(datapoints,function(segment){ - valuesArray.push(segment.value); - }); - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes, - { - size: helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - } - ); - - }, - update : function(){ - this.calculateTotal(this.segments); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - - this.reflow(); - this.render(); - }, - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.updateScaleRange(this.segments); - this.scale.update(); - - helpers.extend(this.scale,{ - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.scale.calculateCenterOffset(segment.value) - }); - }, this); - - }, - draw : function(ease){ - var easingDecimal = ease || 1; - //Clear & draw the canvas - this.clear(); - helpers.each(this.segments,function(segment, index){ - segment.transition({ - circumference : this.scale.getCircumference(), - outerRadius : this.scale.calculateCenterOffset(segment.value) - },easingDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - // If we've removed the first segment we need to set the first one to - // start at the top. - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length - 1){ - this.segments[index+1].startAngle = segment.endAngle; - } - segment.draw(); - }, this); - this.scale.draw(); - } - }); - -}).call(this); -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - - Chart.Type.extend({ - name: "Radar", - defaults:{ - //Boolean - Whether to show lines for each scale point - scaleShowLine : true, - - //Boolean - Whether we show the angle lines out of the radar - angleShowLineOut : true, - - //Boolean - Whether to show labels on the scale - scaleShowLabels : false, - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //String - Colour of the angle line - angleLineColor : "rgba(0,0,0,.1)", - - //Number - Pixel width of the angle line - angleLineWidth : 1, - - //String - Point label font declaration - pointLabelFontFamily : "'Arial'", - - //String - Point label font weight - pointLabelFontStyle : "normal", - - //Number - Point label font size in pixels - pointLabelFontSize : 10, - - //String - Point label font colour - pointLabelFontColor : "#666", - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 3, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }, - - initialize: function(data){ - this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx - }); - - this.datasets = []; - - this.buildScale(data); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePointsCollection, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - - this.showTooltip(activePointsCollection); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label: dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - var pointPosition; - if (!this.scale.animation){ - pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); - } - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, - y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - },this); - - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - - getPointsAtEvent : function(evt){ - var mousePosition = helpers.getRelativePosition(evt), - fromCenter = helpers.getAngleFromPoint({ - x: this.scale.xCenter, - y: this.scale.yCenter - }, mousePosition); - - var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, - pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), - activePointsCollection = []; - - // If we're at the top, make the pointIndex 0 to get the first of the array. - if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ - pointIndex = 0; - } - - if (fromCenter.distance <= this.scale.drawingArea){ - helpers.each(this.datasets, function(dataset){ - activePointsCollection.push(dataset.points[pointIndex]); - }); - } - - return activePointsCollection; - }, - - buildScale : function(data){ - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - angleLineColor : this.options.angleLineColor, - angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, - // Point labels at the edge of each line - pointLabelFontColor : this.options.pointLabelFontColor, - pointLabelFontSize : this.options.pointLabelFontSize, - pointLabelFontFamily : this.options.pointLabelFontFamily, - pointLabelFontStyle : this.options.pointLabelFontStyle, - height : this.chart.height, - width: this.chart.width, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - labels: data.labels, - valuesCount: data.datasets[0].data.length - }); - - this.scale.setScaleSize(); - this.updateScaleRange(data.datasets); - this.scale.buildYLabels(); - }, - updateScaleRange: function(datasets){ - var valuesArray = (function(){ - var totalDataArray = []; - helpers.each(datasets,function(dataset){ - if (dataset.data){ - totalDataArray = totalDataArray.concat(dataset.data); - } - else { - helpers.each(dataset.points, function(point){ - totalDataArray.push(point.value); - }); - } - }); - return totalDataArray; - })(); - - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes - ); - - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - this.scale.valuesCount++; - helpers.each(valuesArray,function(value,datasetIndex){ - var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - x: pointPosition.x, - y: pointPosition.y, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.labels.push(label); - - this.reflow(); - - this.update(); - }, - removeData : function(){ - this.scale.valuesCount--; - this.scale.labels.shift(); - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.reflow(); - this.update(); - }, - update : function(){ - this.eachPoints(function(point){ - point.save(); - }); - this.reflow(); - this.render(); - }, - reflow: function(){ - helpers.extend(this.scale, { - width : this.chart.width, - height: this.chart.height, - size : helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - this.updateScaleRange(this.datasets); - this.scale.setScaleSize(); - this.scale.buildYLabels(); - }, - draw : function(ease){ - var easeDecimal = ease || 1, - ctx = this.chart.ctx; - this.clear(); - this.scale.draw(); - - helpers.each(this.datasets,function(dataset){ - - //Transition each point first so that the line and point drawing isn't out of sync - helpers.each(dataset.points,function(point,index){ - if (point.hasValue()){ - point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); - } - },this); - - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - helpers.each(dataset.points,function(point,index){ - if (index === 0){ - ctx.moveTo(point.x,point.y); - } - else{ - ctx.lineTo(point.x,point.y); - } - },this); - ctx.closePath(); - ctx.stroke(); - - ctx.fillStyle = dataset.fillColor; - ctx.fill(); - - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(dataset.points,function(point){ - if (point.hasValue()){ - point.draw(); - } - }); - - },this); - - } - - }); - - - - - -}).call(this); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3285fdae331..95f20a4d3e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1443,6 +1443,10 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chart.js@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-1.0.2.tgz#ad57d2229cfd8ccf5955147e8121b4911e69dfe7" + chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" From fb38bc3faeee0d691f8f473cf5b32d356eabb43f Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 16:41:36 -0600 Subject: [PATCH 078/161] remove graphs_show webpack bundle --- .../graphs_show.js => pages/projects/graphs/show/index.js} | 6 +++--- .../projects/graphs/show}/stat_graph_contributors.js | 2 +- .../projects/graphs/show}/stat_graph_contributors_graph.js | 0 .../projects/graphs/show}/stat_graph_contributors_util.js | 0 app/views/projects/graphs/show.html.haml | 3 --- config/webpack.config.js | 2 -- 6 files changed, 4 insertions(+), 9 deletions(-) rename app/assets/javascripts/{graphs/graphs_show.js => pages/projects/graphs/show/index.js} (84%) rename app/assets/javascripts/{graphs => pages/projects/graphs/show}/stat_graph_contributors.js (98%) rename app/assets/javascripts/{graphs => pages/projects/graphs/show}/stat_graph_contributors_graph.js (100%) rename app/assets/javascripts/{graphs => pages/projects/graphs/show}/stat_graph_contributors_util.js (100%) diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/pages/projects/graphs/show/index.js similarity index 84% rename from app/assets/javascripts/graphs/graphs_show.js rename to app/assets/javascripts/pages/projects/graphs/show/index.js index b670e907a5c..f516ff20995 100644 --- a/app/assets/javascripts/graphs/graphs_show.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -1,6 +1,6 @@ -import flash from '../flash'; -import { __ } from '../locale'; -import axios from '../lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; import ContributorsStatGraph from './stat_graph_contributors'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js similarity index 98% rename from app/assets/javascripts/graphs/stat_graph_contributors.js rename to app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 151a4ce012c..9ac0b4c07e5 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ import _ from 'underscore'; +import { n__, s__, createDateTimeFormat, sprintf } from '~/locale'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; -import { n__, s__, createDateTimeFormat, sprintf } from '../locale'; export default (function() { function ContributorsStatGraph() { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js similarity index 100% rename from app/assets/javascripts/graphs/stat_graph_contributors_graph.js rename to app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js similarity index 100% rename from app/assets/javascripts/graphs/stat_graph_contributors_util.js rename to app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 91d2c48ccd1..c81ee6874e3 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,8 +1,5 @@ - @no_container = true - page_title _('Contributors') -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_d3') - = webpack_bundle_tag('graphs_show') .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .sub-header-block diff --git a/config/webpack.config.js b/config/webpack.config.js index 225907a544b..9bf9cee7aa8 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -62,7 +62,6 @@ var config = { environments: './environments/environments_bundle.js', environments_folder: './environments/folder/environments_folder_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', - graphs_show: './graphs/graphs_show.js', help: './help/help.js', how_to_merge: './how_to_merge.js', issue_show: './issue_show/index.js', @@ -281,7 +280,6 @@ var config = { new webpack.optimize.CommonsChunkPlugin({ name: 'common_d3', chunks: [ - 'graphs_show', 'monitoring', 'users', ], From 15a4773e06d91d57a80699818a7754e92e276b39 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 16:49:13 -0600 Subject: [PATCH 079/161] remove common_d3 bundle --- app/views/projects/environments/metrics.html.haml | 1 - app/views/projects/graphs/charts.html.haml | 2 -- config/webpack.config.js | 12 ------------ 3 files changed, 15 deletions(-) diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 10812f67cbe..91b3743e9e7 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -2,7 +2,6 @@ - page_title "Metrics for environment", @environment.name - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'common_d3' .prometheus-container{ class: container_class } .top-area diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index efdb494e1ae..d4b4a6203f3 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -1,7 +1,5 @@ - @no_container = true - page_title "Charts" -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_d3') .repo-charts{ class: container_class } %h4.sub-header diff --git a/config/webpack.config.js b/config/webpack.config.js index 9bf9cee7aa8..31b29075d62 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -276,18 +276,6 @@ var config = { }, }), - // create cacheable common library bundle for all d3 chunks - new webpack.optimize.CommonsChunkPlugin({ - name: 'common_d3', - chunks: [ - 'monitoring', - 'users', - ], - minChunks: function (module, count) { - return module.resource && /d3-/.test(module.resource); - }, - }), - // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ names: ['main', 'common', 'webpack_runtime'], From 91bbd80f493236dbcaf206183b55009a810d31d9 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 19 Feb 2018 17:04:08 -0600 Subject: [PATCH 080/161] fix broken specs --- .../projects/graphs/show/stat_graph_contributors_graph.js | 2 +- spec/javascripts/graphs/stat_graph_contributors_graph_spec.js | 2 +- spec/javascripts/graphs/stat_graph_contributors_spec.js | 4 ++-- spec/javascripts/graphs/stat_graph_contributors_util_spec.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index 9a4012232a0..6ffaa277a0a 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -7,7 +7,7 @@ import { axisLeft, axisBottom } from 'd3-axis'; import { area } from 'd3-shape'; import { brushX } from 'd3-brush'; import { timeParse } from 'd3-time-format'; -import { dateTickFormat } from '../lib/utils/tick_formats'; +import { dateTickFormat } from '~/lib/utils/tick_formats'; const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index 6599839a526..d8a8c8cc260 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */ import { scaleLinear, scaleTime } from 'd3-scale'; import { timeParse } from 'd3-time-format'; -import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph'; +import { ContributorsGraph, ContributorsMasterGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph'; const d3 = { scaleLinear, scaleTime, timeParse }; diff --git a/spec/javascripts/graphs/stat_graph_contributors_spec.js b/spec/javascripts/graphs/stat_graph_contributors_spec.js index 962423462e7..e03114c1cc5 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_spec.js @@ -1,5 +1,5 @@ -import ContributorsStatGraph from '~/graphs/stat_graph_contributors'; -import { ContributorsGraph } from '~/graphs/stat_graph_contributors_graph'; +import ContributorsStatGraph from '~/pages/projects/graphs/show/stat_graph_contributors'; +import { ContributorsGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph'; import { setLanguage } from '../helpers/locale_helper'; diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 9b47ab62181..22a9afe1a9d 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,6 +1,6 @@ /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ -import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util'; +import ContributorsStatGraphUtil from '~/pages/projects/graphs/show/stat_graph_contributors_util'; describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { From 0dafea8685f0376d4c474f7670a97d0b8e449767 Mon Sep 17 00:00:00 2001 From: Maxime Roussin-Belanger Date: Sun, 18 Feb 2018 22:17:23 -0500 Subject: [PATCH 081/161] Add missing pagination on the commit diff endpoint --- .../17203-add-missing-pagination-commit-diff-endpoint.yml | 5 +++++ lib/api/commits.rb | 5 ++++- spec/requests/api/commits_spec.rb | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml diff --git a/changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml b/changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml new file mode 100644 index 00000000000..efd936ca104 --- /dev/null +++ b/changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml @@ -0,0 +1,5 @@ +--- + title: Add missing pagination on the commit diff endpoint + merge_request: 17203 + author: Maxime Roussin-Bélanger + type: fixed diff --git a/lib/api/commits.rb b/lib/api/commits.rb index d83c43ee49b..3d6e78d2d80 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -97,13 +97,16 @@ module API end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' + use :pagination end get ':id/repository/commits/:sha/diff', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - present commit.raw_diffs.to_a, with: Entities::Diff + raw_diffs = ::Kaminari.paginate_array(commit.raw_diffs.to_a) + + present paginate(raw_diffs), with: Entities::Diff end desc "Get a commit's comments" do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 31959d28fee..ad3eec88952 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -698,6 +698,7 @@ describe API::Commits do get api(route, current_user) expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers expect(json_response.size).to be >= 1 expect(json_response.first.keys).to include 'diff' end From 2823d2707e9e1a1d728fc7ae3da16edf6b1ba4e7 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Fri, 16 Feb 2018 11:37:33 +1100 Subject: [PATCH 082/161] Remove unecessary validate: true from belongs_to :project This does not seem to serve any clear purpose and causes other issues (see https://gitlab.com/gitlab-org/gitlab-ce/issues/43196#note_59275652) --- app/models/deployment.rb | 4 ++-- app/models/environment.rb | 2 +- .../unreleased/remove-unnecessary-validate-project.yml | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/remove-unnecessary-validate-project.yml diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 3aed071dd49..b6cf168d60e 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -1,8 +1,8 @@ class Deployment < ActiveRecord::Base include InternalId - belongs_to :project, required: true, validate: true - belongs_to :environment, required: true, validate: true + belongs_to :project, required: true + belongs_to :environment, required: true belongs_to :user belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations diff --git a/app/models/environment.rb b/app/models/environment.rb index 2f6eae605ee..f78c21aebe5 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -4,7 +4,7 @@ class Environment < ActiveRecord::Base NUMBERS = '0'..'9' SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a - belongs_to :project, required: true, validate: true + belongs_to :project, required: true has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/changelogs/unreleased/remove-unnecessary-validate-project.yml b/changelogs/unreleased/remove-unnecessary-validate-project.yml new file mode 100644 index 00000000000..ebc8da03dd8 --- /dev/null +++ b/changelogs/unreleased/remove-unnecessary-validate-project.yml @@ -0,0 +1,5 @@ +--- +title: 'Remove unecessary validate: true from belongs_to :project' +merge_request: +author: +type: fixed From 6ceb2cb3c0f84bd78161c01ed97efae079c8a3ba Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 20 Feb 2018 17:32:25 +1100 Subject: [PATCH 083/161] only show error if there is an error --- app/assets/javascripts/single_file_diff.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 48dd91bdf06..0cd6e69811a 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -88,6 +88,8 @@ export default class SingleFileDiff { if (cb) cb(); }) - .catch(createFlash(__('An error occurred while retrieving diff'))); + .catch(() => { + createFlash(__('An error occurred while retrieving diff')); + }); } } From a55106d629bde3d7d39ca7ec126f955e9a08b7bd Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Tue, 20 Feb 2018 01:48:06 -0500 Subject: [PATCH 084/161] Update Prometheus docs for enhanced integration --- .../integrations/img/prometheus_dashboard.png | Bin 0 -> 63234 bytes .../integrations/img/prometheus_deploy.png | Bin 0 -> 27258 bytes doc/user/project/integrations/prometheus.md | 142 ++++++------------ .../prometheus_library/kubernetes.md | 9 +- .../integrations/samples/prometheus.yml | 107 ------------- 5 files changed, 52 insertions(+), 206 deletions(-) create mode 100644 doc/user/project/integrations/img/prometheus_dashboard.png create mode 100644 doc/user/project/integrations/img/prometheus_deploy.png delete mode 100644 doc/user/project/integrations/samples/prometheus.yml diff --git a/doc/user/project/integrations/img/prometheus_dashboard.png b/doc/user/project/integrations/img/prometheus_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..84eac5eb93979da167923d150db8b6f8d950fccc GIT binary patch literal 63234 zcmbUI1yCfx*0zf_Fu1$h;4Xu^Gq}6EyEN`H(8vt#I>6xW?(XjH?hc2&zwf{2MBF%W zBW_1@S9MiZ*6PZYZ?1fDb%dh4BqAIh8~^}7l=>;A3;;mX007`^FyLQD^s+H50Dy;K zDKTMH53sX^Kqvqyium-$XSR1OX$?Roi3b`1$HP$v4A6D2{q=!t$^rlk83O>n`vCwF z5EwuRi%JWc*KFm^hY$d$c~X%nc+5~y#q|oK`~F&QauDN2{Z&NpEE7@}U+bf5vw5?; zQLoYcyusch*$eb-kyhU{nbz&^#p1biv^X3&3Mq=NG|e0$kOnKGNtSi{bHlW!(DwWi z*m$XvzL{?EL%1W`z#sQ#xrCfBj&Nw&r+Jpgzt-!ng`DW=B3@`yE{nI$5PfyJ3l54K zI%%8N4~z9gKSFP4q%7Jsp9l5Bs+Ub>i>PixW*h`4n`(hS-nG>GvoyDD?d`gVP0ik1 zOY}GEuF8ee(X-2kua#ar;<`Qj5Y$WJiF3|-CbaZf{^prk78(A|qy9GB?}3(8jSYo` z^|i5YOv^+S!QwpHJfTX%7#9D@lQ<%@Ui+asK#i zXwVy-ea(!21#P3kPqW&;K?Of!1A@B^tFV&ybpZgMhew*NWT8oik1N(UZz3`gk>3h4 zONT6#s79~N=+H6#+2jODd^``+N5|E<*II;G+k*;P_IIhHxY97rXn6oY7B3T;MDS0z z-R()1?b!!p&li4NL5O0x4n+jpl-K9`$>UR#U4O6Hz4vx^u;Q*P4Q;BKY_xmi?AT8C zI5ip!bg0>R{>hMCQ3R+jtC&;#%^5n9u#o{(_bckNa}QPR<@NQ5Gb<}I(==j-Zhs|2 zQShP#{!pWWB*cu%V1Gmtx%f`GcCOYxT1%<}xQ?bPM4dKUGLV_jx46UP=<~gVaBa@4qxfEC=B% zv#SZ%OF5`mpa5IMn3Xep8Z2PN6tL0ioxjve)u`v-_M70zVS6bPq(v&o{o1I$ktR`# zqbn^Tdu=ptPG1uAESz

Y&K?eP-!w!X1Gda_-fyB6!? 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ēlR6~P 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 literal 0 HcmV?d00001 diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 5fefb3b69c4..a0358a33f8d 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -2,17 +2,54 @@ > [Introduced][ce-8935] in GitLab 9.0. -GitLab offers powerful integration with [Prometheus] for monitoring your apps. -Metrics are retrieved from the configured Prometheus server, and then displayed +GitLab offers powerful integration with [Prometheus] for monitoring key metrics your apps, directly within GitLab. +Metrics for each environment are retrieved from Prometheus, and then displayed within the GitLab interface. -Each project can be configured with its own specific Prometheus server, see the -[configuration](#configuration) section for more details. If you have a single -Prometheus server which monitors all of your infrastructure, you can pre-fill -the settings page with a default template. To configure the template, see the -[Services templates](services_templates.md) document. +![Environment Dashboard](img/prometheus_dashboard.png) -## Requirements +There are two ways to setup Prometheus integration, depending on where your apps are running: +* For deployments on Kubernetes, GitLab can [deploy and manage Prometheus](#managed-prometheus-on-kubernetes) in a cluster +* For other deployment targets, simply [specify the Prometheus server](manual-configuration-of-prometheus). + +## Managed Prometheus on Kubernetes + +GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cluster](../clusters/index.html), making monitoring the metrics of your deployed apps as easy as a single click. + +### Requirements + +* GitLab [10.5 or above](https://gitlab.com/gitlab-org/gitlab-ce/issues/28916) +* A [connected Kubernetes cluster](../clusters/index.html) +* Helm Tiller [installed by GitLab](../clusters/index.html#installing-applications) + +### Getting started + +Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click. + +1. Go to the `CI/CD > Kubernetes` page, to view your connected clusters +1. Select the cluster you would like to deploy Prometheus to +1. Click the **Install** button to deploy Prometheus to the cluster + +![Managed Prometheus Deploy](img/prometheus_deploy.png) + +### About managed Prometheus deployments + +Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/). + +The Prometheus server will [automatically detect and monitor](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#%3Ckubernetes_sd_config%3E) nodes, pods, and endpoints. + +To configure a resource to be monitored by Prometheus, simply set the following [Kubernetes annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/): +* `prometheus.io/scrape` to `true` to enable monitoring of the resource. +* `prometheus.io/port` to define the port of the metrics endpoint. +* `prometheus.io/path` to define the path of the metrics endpoint. Defaults to `/metrics`. + +CPU and Memory consumption is also monitored, but requires [naming conventions](prometheus_library/kubernetes.html#specifying-the-environment) in order to determine the environment. If you are using [Auto DevOps](../../../topics/autodevops/), this is handled automatically. + +The [NGINX Ingress]((../clusters/index.html#installing-applications)) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates. + +## Manual configuration of Prometheus + +### Requirements Integration with Prometheus requires the following: @@ -21,15 +58,7 @@ Integration with Prometheus requires the following: 1. Each metric must be have a label to indicate the environment 1. GitLab must have network connectivity to the Prometheus server -## Getting started with Prometheus monitoring - -Depending on your deployment and where you have located your GitLab server, there are a few options to get started with Prometheus monitoring. - -* If both GitLab and your applications are installed in the same Kubernetes cluster, you can leverage the [bundled Prometheus server within GitLab](#configuring-omnibus-gitlab-prometheus-to-monitor-kubernetes). -* If your applications are deployed on Kubernetes, but GitLab is not in the same cluster, then you can [configure a Prometheus server in your Kubernetes cluster](#configuring-your-own-prometheus-server-within-kubernetes). -* If your applications are not running in Kubernetes, [get started with Prometheus](#getting-started-with-prometheus-outside-of-kubernetes). - -### Getting started with Prometheus outside of Kubernetes +### Getting started Installing and configuring Prometheus to monitor applications is fairly straight forward. @@ -37,84 +66,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight 1. Set up one of the [supported monitoring targets](prometheus_library/metrics.md) 1. Configure the Prometheus server to [collect their metrics](https://prometheus.io/docs/operating/configuration/#scrape_config) -### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments - -With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled -version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#). - -1. Read how to configure the bundled Prometheus server in the - [Administration guide][gitlab-prometheus-k8s-monitor]. -1. Now that Prometheus is configured, proceed on - [configuring the Prometheus project service in GitLab](#configuration-in-gitlab). - -### Configuring your own Prometheus server within Kubernetes - -Setting up and configuring Prometheus within Kubernetes is quick and painless. -The Prometheus project provides an [official Docker image][prometheus-docker-image] -which we can use as a starting point. - -To get started quickly, we have provided a [sample YML file][prometheus-yml] -that can be used as a template. This file will create a `prometheus` **Namespace**, -**Service**, **Deployment**, and **ConfigMap** in Kubernetes. You can upload -this file to the Kubernetes dashboard using **+ Create** at the top right. - -![Deploy Prometheus](img/prometheus_yaml_deploy.png) - -Or use `kubectl`: - -```bash -kubectl apply -f path/to/prometheus.yml -``` - -Once deployed, you should see the Prometheus service, deployment, and -pod start within the `prometheus` namespace. The server will begin to collect -metrics from each Kubernetes Node in the cluster, based on the configuration -provided in the template. It will also attempt to collect metrics from any Kubernetes Pods that have been [annotated for Prometheus](https://prometheus.io/docs/operating/configuration/#pod). - -Since GitLab is not running within Kubernetes, the template provides external -network access via a `NodePort` running on `30090`. This method allows access -to be controlled using provider firewall rules, like within Google Compute Engine. - -Since a `NodePort` does not automatically have firewall rules created for it, -one will need to be created manually to allow access. In GCP/GKE, you will want -to confirm the Node that the Prometheus pod is running on. This can be done -either by looking at the Pod in the Kubernetes dashboard, or by running: - -```bash -kubectl describe pods -n prometheus -``` - -Next on GKE, we need to get the `tag` of the Node or VM Instance, so we can -create an accurate firewall rule. The easiest way to do this is to go into the -Google Cloud Platform Compute console and select the VM instance that matches -the name of the Node gathered from the step above. In this case, the node tag -needed is `gke-prometheus-demo-5d5ada10-node`. Also make a note of the -**External IP**, which will be the IP address the Prometheus server is reachable -on. - -![GCP Node Detail](img/prometheus_gcp_node_name.png) - -Armed with the proper Node tag, the firewall rule can now be created -specifically for this node. To create the firewall rule, open the Google Cloud -Platform Networking console, and select **Firewall Rules**. - -Create a new rule: - -- Specify the source IP range to match your desired access list, which should - include your GitLab server. A sample of GitLab.com's IP address range is - available [in this issue][gitlab.com-ip-range], but note that GitLab.com's IPs - are subject to change without prior notification. -- Allowed protocol and port should be `tcp:30090`. -- The target tags should match the Node tag identified earlier in this step. - -![GCP Firewall Rule](img/prometheus_gcp_firewall_rule.png) - ---- - -Now that Prometheus is configured, proceed to -[configure the Prometheus project service in GitLab](##configuration-in-gitlab). - -## Configuration in GitLab +### Configuration in GitLab The actual configuration of Prometheus integration within GitLab is very simple. All you will need is the DNS or IP address of the Prometheus server you'd like diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index a6673fa2a00..81d022314c2 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -24,9 +24,10 @@ Prometheus server up and running. You have two options here: - If you have an Omnibus based GitLab installation within your Kubernetes cluster, you can leverage the bundled Prometheus server to [monitor Kubernetes](../../../../administration/monitoring/prometheus/index.md#configuring-prometheus-to-monitor-kubernetes). - To configure your own Prometheus server, you can follow the [Prometheus documentation](https://prometheus.io/docs/introduction/overview/) or [our guide](../../../../administration/monitoring/prometheus/index.md#configuring-your-own-prometheus-server-within-kubernetes). -## Specifying the Environment label +## Specifying the Environment -In order to isolate and only display relevant metrics for a given environment -however, GitLab needs a method to detect which labels are associated. To do this, GitLab will [look for an `environment` label](metrics.md#identifying-environments). +In order to isolate and only display relevant CPU and Memory metrics for a given environment, GitLab needs a method to detect which containers it is running. Because these metrics are tracked at the container level, traditional Kubernetes labels are not available. -If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. +Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with the name of the [enviroment](../../../../environments.html). It can be followed by a `-` and additional content if desired. + +If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the naming will be correctly set automatically. diff --git a/doc/user/project/integrations/samples/prometheus.yml b/doc/user/project/integrations/samples/prometheus.yml deleted file mode 100644 index 3a4735d282f..00000000000 --- a/doc/user/project/integrations/samples/prometheus.yml +++ /dev/null @@ -1,107 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: prometheus ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: prometheus - namespace: prometheus -data: - prometheus.yml: |- - scrape_configs: - - job_name: 'kubernetes-nodes' - scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - insecure_skip_verify: true - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - kubernetes_sd_configs: - - role: node - metric_relabel_configs: - - source_labels: [pod_name] - target_label: environment - regex: (.+)-.+-.+ - replacement: $1 - - job_name: kubernetes-pods - tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - insecure_skip_verify: true - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" - kubernetes_sd_configs: - - role: pod - api_server: https://kubernetes.default.svc:443 - tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" - relabel_configs: - - source_labels: - - __meta_kubernetes_pod_annotation_prometheus_io_scrape - action: keep - regex: 'true' - - source_labels: - - __meta_kubernetes_pod_annotation_prometheus_io_path - action: replace - target_label: __metrics_path__ - regex: "(.+)" - - source_labels: - - __address__ - - __meta_kubernetes_pod_annotation_prometheus_io_port - action: replace - regex: "([^:]+)(?::[0-9]+)?;([0-9]+)" - replacement: "$1:$2" - target_label: __address__ - - action: labelmap - regex: __meta_kubernetes_pod_label_(.+) - - source_labels: - - __meta_kubernetes_namespace - action: replace - target_label: kubernetes_namespace - - source_labels: - - __meta_kubernetes_pod_name - action: replace - target_label: kubernetes_pod_name ---- -apiVersion: v1 -kind: Service -metadata: - name: prometheus - namespace: prometheus -spec: - selector: - app: prometheus - ports: - - name: prometheus - protocol: TCP - port: 9090 - nodePort: 30090 - type: NodePort ---- -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - name: prometheus - namespace: prometheus -spec: - replicas: 1 - template: - metadata: - labels: - app: prometheus - spec: - containers: - - name: prometheus - image: prom/prometheus:latest - args: - - '--config.file=/prometheus-data/prometheus.yml' - ports: - - name: prometheus - containerPort: 9090 - volumeMounts: - - name: data-volume - mountPath: /prometheus-data - volumes: - - name: data-volume - configMap: - name: prometheus From c7fd85a7790ac821ce5cdaee245fae7e6b9f8d5a Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Tue, 20 Feb 2018 01:56:40 -0500 Subject: [PATCH 085/161] Fix links --- doc/user/project/integrations/prometheus.md | 10 +++++----- .../integrations/prometheus_library/kubernetes.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index a0358a33f8d..8ddebf89c76 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -10,17 +10,17 @@ within the GitLab interface. There are two ways to setup Prometheus integration, depending on where your apps are running: * For deployments on Kubernetes, GitLab can [deploy and manage Prometheus](#managed-prometheus-on-kubernetes) in a cluster -* For other deployment targets, simply [specify the Prometheus server](manual-configuration-of-prometheus). +* For other deployment targets, simply [specify the Prometheus server](#manual-configuration-of-prometheus). ## Managed Prometheus on Kubernetes -GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cluster](../clusters/index.html), making monitoring the metrics of your deployed apps as easy as a single click. +GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cluster](../clusters/index.md), making monitoring the metrics of your deployed apps as easy as a single click. ### Requirements * GitLab [10.5 or above](https://gitlab.com/gitlab-org/gitlab-ce/issues/28916) -* A [connected Kubernetes cluster](../clusters/index.html) -* Helm Tiller [installed by GitLab](../clusters/index.html#installing-applications) +* A [connected Kubernetes cluster](../clusters/index.md) +* Helm Tiller [installed by GitLab](../clusters/index.md#installing-applications) ### Getting started @@ -45,7 +45,7 @@ To configure a resource to be monitored by Prometheus, simply set the following CPU and Memory consumption is also monitored, but requires [naming conventions](prometheus_library/kubernetes.html#specifying-the-environment) in order to determine the environment. If you are using [Auto DevOps](../../../topics/autodevops/), this is handled automatically. -The [NGINX Ingress]((../clusters/index.html#installing-applications)) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates. +The [NGINX Ingress](../clusters/index.md#installing-applications) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates. ## Manual configuration of Prometheus diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index 81d022314c2..8fbaa1a4a34 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -28,6 +28,6 @@ Prometheus server up and running. You have two options here: In order to isolate and only display relevant CPU and Memory metrics for a given environment, GitLab needs a method to detect which containers it is running. Because these metrics are tracked at the container level, traditional Kubernetes labels are not available. -Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with the name of the [enviroment](../../../../environments.html). It can be followed by a `-` and additional content if desired. +Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with the name of the [environment](../../../../ci/environments.md). It can be followed by a `-` and additional content if desired. If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the naming will be correctly set automatically. From 7c9070a96523c8746d4945cc2029c15f93b0e82b Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Tue, 20 Feb 2018 02:08:57 -0500 Subject: [PATCH 086/161] Minor polish --- .../integrations/img/prometheus_dashboard.png | Bin 63234 -> 26112 bytes doc/user/project/integrations/prometheus.md | 10 ++++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/doc/user/project/integrations/img/prometheus_dashboard.png b/doc/user/project/integrations/img/prometheus_dashboard.png index 84eac5eb93979da167923d150db8b6f8d950fccc..bd19f1b44cc02543228173010a0f86c6902505e0 100644 GIT binary patch literal 26112 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%0ANCJBIgk0k*#!Hap@Rx8YaHtJ@miW>gU$CNoA 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

Y&K?eP-!w!X1Gda_-fyB6!? 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ēlR6~P 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 r2=S@^9$m Date: Tue, 20 Feb 2018 10:02:11 +0100 Subject: [PATCH 087/161] Add version which LFS lock was introduced --- doc/workflow/lfs/manage_large_binaries_with_git_lfs.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 8fff3d591fe..377eee69c11 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -34,7 +34,7 @@ Documentation for GitLab instance administrators is under [LFS administration do credentials store is recommended * Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see [troubleshooting](#troubleshooting)) - + >**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication still goes over HTTP, but now the SSH client passes the correct credentials to the Git LFS client, so no action is required by the user. @@ -85,6 +85,8 @@ git lfs fetch master ## File Locking +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35856) in GitLab 10.5. + The first thing to do before using File Locking is to tell Git LFS which kind of files are lockable. The following command will store PNG files in LFS and flag them as lockable: From bb9e7a3f2cf80c09b64156988abb9ffba1de6d2c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 16 Feb 2018 12:07:04 +0000 Subject: [PATCH 088/161] CE port of changes made to the pipeline bundle in EE - Fixes typos and adds i18n Backport common class name for the tab content Backport more changes --- .../pipelines/components/graph/graph_component.vue | 2 +- .../javascripts/pipelines/pipeline_details_bundle.js | 10 +++++++--- ...ils_mediatior.js => pipeline_details_mediator.js} | 3 ++- app/assets/stylesheets/pages/pipelines.scss | 9 ++++++--- app/views/projects/pipelines/_with_tabs.html.haml | 12 ++++++------ .../pipelines/pipeline_details_mediator_spec.js | 2 +- spec/javascripts/pipelines/pipeline_store_spec.js | 1 - 7 files changed, 23 insertions(+), 16 deletions(-) rename app/assets/javascripts/pipelines/{pipeline_details_mediatior.js => pipeline_details_mediator.js} (93%) diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index a1f58580318..ab84711d4a2 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -52,7 +52,7 @@