From 18fa0c91cec0e3c185eeb3358e5b25f160e4de05 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Wed, 24 Jan 2018 01:12:06 +0900 Subject: Reduce Spinach parallelization to 2 nodes and increase RSpec's to 28 nodes --- .gitlab-ci.yml | 126 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 63 insertions(+), 63 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-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 - -spinach-pg 0 3: *spinach-metadata-pg -spinach-pg 1 3: *spinach-metadata-pg -spinach-pg 2 3: *spinach-metadata-pg - -spinach-mysql 0 3: *spinach-metadata-mysql -spinach-mysql 1 3: *spinach-metadata-mysql -spinach-mysql 2 3: *spinach-metadata-mysql +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 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 2: *spinach-metadata-pg +spinach-pg 1 2: *spinach-metadata-pg + +spinach-mysql 0 2: *spinach-metadata-mysql +spinach-mysql 1 2: *spinach-metadata-mysql # Static analysis jobs .ruby-static-analysis: &ruby-static-analysis -- cgit v1.2.1 From 73f999d436b438b61efed262fba70497c40808e9 Mon Sep 17 00:00:00 2001 From: Lars Kumbier Date: Sat, 10 Feb 2018 16:15:15 +0100 Subject: 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 ``` -- cgit v1.2.1 From d9763cb4dfb2eaf78718abc21434a3fde20a4fb6 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 7 Feb 2018 15:39:54 -0500 Subject: Refactor issue index filtered search --- app/assets/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") -- cgit v1.2.1 From 9bfa690b7d0a14bdd2791fe62f46dd38f57aa23e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 9 Feb 2018 15:58:28 +0100 Subject: add project import spec --- spec/requests/api/project_import_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 spec/requests/api/project_import_spec.rb diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb new file mode 100644 index 00000000000..51980dc1872 --- /dev/null +++ b/spec/requests/api/project_import_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe API::ProjectImport do + let(:export_path) { "#{Dir.tmpdir}/project_export_spec" } + let(:user) { create(:user) } + let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } + let(:namespace){ create(:group) } + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + group.add_owner(user) + end + + after do + FileUtils.rm_rf(export_path, secure: true) + end + + it 'schedules an import' do + expect_any_instance_of(Project).to receive(:import_schedule) + + post "/projects/import", file: file, namespace: namespace.full_path + + expect(project.status).to eq('started') + end +end -- cgit v1.2.1 From de3edb7178bd7df2f72ef403e57bfdf4e6f732df Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 12 Feb 2018 10:13:08 +0100 Subject: add more specs --- spec/requests/api/project_import_spec.rb | 34 +++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 51980dc1872..bf7bde0325a 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -4,7 +4,7 @@ describe API::ProjectImport do let(:export_path) { "#{Dir.tmpdir}/project_export_spec" } let(:user) { create(:user) } let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } - let(:namespace){ create(:group) } + let(:namespace) { create(:group) } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) @@ -15,11 +15,35 @@ describe API::ProjectImport do FileUtils.rm_rf(export_path, secure: true) end - it 'schedules an import' do - expect_any_instance_of(Project).to receive(:import_schedule) + describe 'POST /projects/import' do - post "/projects/import", file: file, namespace: namespace.full_path + it 'schedules an import' do + expect_any_instance_of(Project).to receive(:import_schedule) - expect(project.status).to eq('started') + post api('/projects/import', user), file: file, namespace: namespace.full_path + + expect(project.status).to eq('started') + end + end + + describe 'GET /projects/:id/import' do + it 'returns the import status' do + project = create(:project, import_status: 'started') + + get api("/projects/#{project.id}/import", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq('import_status' => 'started') + end + + it 'returns the import status and the error if failed' do + project = create(:project, import_status: 'failed', import_error: 'error') + + get api("/projects/#{project.id}/import", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq('import_status' => 'failed', + 'import_error' => 'error') + end end end -- cgit v1.2.1 From 848f49801d2c45227c525fb5ecdf8b71e7711ded Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 12 Feb 2018 12:40:55 +0100 Subject: add entity and update spec --- lib/api/api.rb | 1 + lib/api/entities.rb | 5 +++++ spec/requests/api/project_import_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/api/api.rb b/lib/api/api.rb index e953f3d2eca..754549f72f0 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -138,6 +138,7 @@ module API mount ::API::PagesDomains mount ::API::Pipelines mount ::API::PipelineSchedules + mount ::API::ProjectImport mount ::API::ProjectHooks mount ::API::Projects mount ::API::ProjectMilestones diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 7838de13c56..8fbad2b6959 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -90,6 +90,11 @@ module API expose :created_at end + class ProjectImportStatus < ProjectIdentity + expose :import_status + expose :import_error, if: :import_error + end + class BasicProjectDetails < ProjectIdentity include ::API::ProjectsRelationBuilder diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index bf7bde0325a..55df2d34419 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -33,7 +33,7 @@ describe API::ProjectImport do get api("/projects/#{project.id}/import", user) expect(response).to have_gitlab_http_status(200) - expect(json_response).to eq('import_status' => 'started') + expect(json_response).to include('import_status' => 'started') end it 'returns the import status and the error if failed' do @@ -42,7 +42,7 @@ describe API::ProjectImport do get api("/projects/#{project.id}/import", user) expect(response).to have_gitlab_http_status(200) - expect(json_response).to eq('import_status' => 'failed', + expect(json_response).to include('import_status' => 'failed', 'import_error' => 'error') end end -- cgit v1.2.1 From 82ff66ef31d6ff8ba2332f51b38f79b3bd7d64a5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 12 Feb 2018 12:42:33 +0100 Subject: add post import API endpoint --- lib/api/project_import.rb | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 lib/api/project_import.rb diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb new file mode 100644 index 00000000000..4bbb78a62f9 --- /dev/null +++ b/lib/api/project_import.rb @@ -0,0 +1,48 @@ +module API + class ProjectImport < Grape::API + include PaginationParams + + helpers do + def import_params + declared_params(include_missing: false) + end + + def file_is_valid? + import_params[:file] && import_params[:file].respond_to?(:read) + end + end + + before do + not_found! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project') + end + + params do + optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' + requires :file, type: File, desc: 'The project export file to be imported' + end + resource :projects do + desc 'Get export status' do + success Entities::ProjectImportStatus + end + post 'import' do + render_api_error!('The branch refname is invalid', 400) unless file_is_valid? + + namespace = import_params[:namespace] + + namespace = if namespace && namespace =~ /^\d+$/ + Namespace.find_by(id: namespace) + elsif namespace.blank? + current_user.namespace + else + Namespace.find_by_path_or_name(namespace) + end + + project = ::Projects::GitlabProjectsImportService.new(current_user, import_params).execute + + render_api_error!(link.project.full_messages.first, 400) unless project.saved? + + present project, with: Entities::ProjectImportStatus + end + end + end +end -- cgit v1.2.1 From d3b3f5d1b4def5e87f90a2347acc0b0ee8edc80a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 12 Feb 2018 14:46:47 +0100 Subject: update import API and spec --- lib/api/project_import.rb | 7 +++++-- spec/requests/api/project_import_spec.rb | 5 ++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 4bbb78a62f9..396c316af22 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -17,6 +17,7 @@ module API end params do + requires :name, type: String, desc: 'The new project name' optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' requires :file, type: File, desc: 'The project export file to be imported' end @@ -37,9 +38,11 @@ module API Namespace.find_by_path_or_name(namespace) end - project = ::Projects::GitlabProjectsImportService.new(current_user, import_params).execute + project_params = import_params.merge(namespace: namespace.id) - render_api_error!(link.project.full_messages.first, 400) unless project.saved? + project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute + + render_api_error!(project.full_messages.first, 400) unless project.saved? present project, with: Entities::ProjectImportStatus end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 55df2d34419..b810c81108c 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -16,11 +16,10 @@ describe API::ProjectImport do end describe 'POST /projects/import' do - it 'schedules an import' do expect_any_instance_of(Project).to receive(:import_schedule) - post api('/projects/import', user), file: file, namespace: namespace.full_path + post api('/projects/import', user), name: 'test', file: file, namespace: namespace.full_path expect(project.status).to eq('started') end @@ -43,7 +42,7 @@ describe API::ProjectImport do expect(response).to have_gitlab_http_status(200) expect(json_response).to include('import_status' => 'failed', - 'import_error' => 'error') + 'import_error' => 'error') end end end -- cgit v1.2.1 From 516d33f5ac8f65d8d69d1e5e88efbf0faabbe0eb Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 12 Feb 2018 15:26:59 +0100 Subject: update import API and spec --- lib/api/project_import.rb | 4 ++-- spec/requests/api/project_import_spec.rb | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 396c316af22..1b63f4d4d9f 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -17,7 +17,7 @@ module API end params do - requires :name, type: String, desc: 'The new project name' + requires :path, type: String, desc: 'The new project path and name' optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' requires :file, type: File, desc: 'The project export file to be imported' end @@ -38,7 +38,7 @@ module API Namespace.find_by_path_or_name(namespace) end - project_params = import_params.merge(namespace: namespace.id) + project_params = import_params.merge(namespace_id: namespace.id) project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index b810c81108c..0875c1cc149 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -8,7 +8,7 @@ describe API::ProjectImport do before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) - group.add_owner(user) + namespace.add_owner(user) end after do @@ -19,9 +19,11 @@ describe API::ProjectImport do it 'schedules an import' do expect_any_instance_of(Project).to receive(:import_schedule) - post api('/projects/import', user), name: 'test', file: file, namespace: namespace.full_path + post api('/projects/import', user), path: 'test-import', file: file, namespace: namespace.full_path - expect(project.status).to eq('started') + expect(response).to have_gitlab_http_status(200) + + expect(Project.find_by_name('test-import').first.status).to eq('started') end end -- cgit v1.2.1 From 7ec1a022b79c68dd3232c0abf07d119f0dad808f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 12 Feb 2018 16:02:15 +0100 Subject: fix file upload --- lib/api/project_import.rb | 7 ++++--- spec/requests/api/project_import_spec.rb | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 1b63f4d4d9f..5a4e4189a58 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -8,7 +8,7 @@ module API end def file_is_valid? - import_params[:file] && import_params[:file].respond_to?(:read) + import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read) end end @@ -26,7 +26,7 @@ module API success Entities::ProjectImportStatus end post 'import' do - render_api_error!('The branch refname is invalid', 400) unless file_is_valid? + render_api_error!('The file is invalid', 400) unless file_is_valid? namespace = import_params[:namespace] @@ -38,7 +38,8 @@ module API Namespace.find_by_path_or_name(namespace) end - project_params = import_params.merge(namespace_id: namespace.id) + project_params = import_params.merge(namespace_id: namespace.id, + file: import_params[:file]['tempfile']) project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 0875c1cc149..3c9a05f93ea 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -19,9 +19,9 @@ describe API::ProjectImport do it 'schedules an import' do expect_any_instance_of(Project).to receive(:import_schedule) - post api('/projects/import', user), path: 'test-import', file: file, namespace: namespace.full_path + post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(201) expect(Project.find_by_name('test-import').first.status).to eq('started') end -- cgit v1.2.1 From 583ed0eb94ff938b4986491e27af5f3c97ea6baf Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 09:24:10 +0100 Subject: add import status endpoint --- lib/api/project_import.rb | 26 ++++++++++++++++++-------- spec/requests/api/project_import_spec.rb | 4 ++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 5a4e4189a58..d554d6d12bd 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -16,12 +16,13 @@ module API not_found! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project') end - params do - requires :path, type: String, desc: 'The new project path and name' - optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' - requires :file, type: File, desc: 'The project export file to be imported' - end - resource :projects do + resource :projects, requirements: { id: %r{[^/]+} } do + + params do + requires :path, type: String, desc: 'The new project path and name' + optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' + requires :file, type: File, desc: 'The project export file to be imported' + end desc 'Get export status' do success Entities::ProjectImportStatus end @@ -40,13 +41,22 @@ module API project_params = import_params.merge(namespace_id: namespace.id, file: import_params[:file]['tempfile']) - project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute - render_api_error!(project.full_messages.first, 400) unless project.saved? + render_api_error!(project&.full_messages&.first, 400) unless project&.saved? present project, with: Entities::ProjectImportStatus end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + desc 'Get export status' do + success Entities::ProjectImportStatus + end + get ':id/import' do + present user_project, with: Entities::ProjectImportStatus + end end end end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 3c9a05f93ea..cbea0229b09 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -22,14 +22,13 @@ describe API::ProjectImport do post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path expect(response).to have_gitlab_http_status(201) - - expect(Project.find_by_name('test-import').first.status).to eq('started') end end describe 'GET /projects/:id/import' do it 'returns the import status' do project = create(:project, import_status: 'started') + project.add_master(user) get api("/projects/#{project.id}/import", user) @@ -39,6 +38,7 @@ describe API::ProjectImport do it 'returns the import status and the error if failed' do project = create(:project, import_status: 'failed', import_error: 'error') + project.add_master(user) get api("/projects/#{project.id}/import", user) -- cgit v1.2.1 From 4a0d56daacc3f1825c5f9644a0ea63842fa9da7d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 09:47:47 +0100 Subject: fix entity --- lib/api/entities.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8fbad2b6959..ffdb06b8094 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -92,7 +92,9 @@ module API class ProjectImportStatus < ProjectIdentity expose :import_status - expose :import_error, if: :import_error + + # TODO: Use `expose_nil` once we upgrade the grape-entity gem + expose :import_error, if: lambda { |status, _ops| status.import_error } end class BasicProjectDetails < ProjectIdentity -- cgit v1.2.1 From 79879145e5cfb3837b3a2f77fb7c35bd1234068c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 10:54:04 +0100 Subject: add more specs --- lib/api/project_import.rb | 10 ++++------ spec/requests/api/project_import_spec.rb | 33 +++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index d554d6d12bd..1e51c92cbf1 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -17,7 +17,6 @@ module API end resource :projects, requirements: { id: %r{[^/]+} } do - params do requires :path, type: String, desc: 'The new project path and name' optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' @@ -30,11 +29,10 @@ module API render_api_error!('The file is invalid', 400) unless file_is_valid? namespace = import_params[:namespace] - - namespace = if namespace && namespace =~ /^\d+$/ - Namespace.find_by(id: namespace) - elsif namespace.blank? + namespace = if namespace.blank? current_user.namespace + elsif namespace =~ /^\d+$/ + Namespace.find_by(id: namespace) else Namespace.find_by_path_or_name(namespace) end @@ -43,7 +41,7 @@ module API file: import_params[:file]['tempfile']) project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute - render_api_error!(project&.full_messages&.first, 400) unless project&.saved? + render_api_error!(project.errors.full_messages&.first, 400) unless project.saved? present project, with: Entities::ProjectImportStatus end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index cbea0229b09..b7b22e91bbf 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -16,13 +16,44 @@ describe API::ProjectImport do end describe 'POST /projects/import' do - it 'schedules an import' do + it 'schedules an import using a namespace' do expect_any_instance_of(Project).to receive(:import_schedule) + expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(namespace.id, any_args).and_call_original post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path expect(response).to have_gitlab_http_status(201) end + + it 'schedules an import at the user namespace level' do + expect_any_instance_of(Project).to receive(:import_schedule) + expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(user.namespace.id, any_args).and_call_original + + post api('/projects/import', user), path: 'test-import2', file: fixture_file_upload(file) + + expect(response).to have_gitlab_http_status(201) + end + + it 'does not schedule an import if the user has no permission to the namespace' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + + post(api('/projects/import', create(:user)), + path: 'test-import3', + file: fixture_file_upload(file), + namespace: namespace.full_path) + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']).to eq('Namespace is not valid') + end + + it 'does not schedule an import if the user uploads no valid file' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + + post api('/projects/import', user), path: 'test-import3', file: './random/test' + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('file is invalid') + end end describe 'GET /projects/:id/import' do -- cgit v1.2.1 From de83f29a3506af01af31c6668640fbb2a0dd54a9 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 11:24:14 +0100 Subject: add more specs --- spec/requests/api/project_import_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index b7b22e91bbf..47c1edc7919 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -20,6 +20,15 @@ describe API::ProjectImport do expect_any_instance_of(Project).to receive(:import_schedule) expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(namespace.id, any_args).and_call_original + post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.id + + expect(response).to have_gitlab_http_status(201) + end + + it 'schedules an import using the namespace path' do + expect_any_instance_of(Project).to receive(:import_schedule) + expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(namespace.id, any_args).and_call_original + post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path expect(response).to have_gitlab_http_status(201) -- cgit v1.2.1 From 17e5ef4b269f4c13d22bd5c10086c6226dc43543 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 15:04:19 +0100 Subject: add docs and changelog --- ...api-endpoint-for-importing-a-project-export.yml | 5 ++ doc/api/project_import_export.md | 72 ++++++++++++++++++++++ lib/api/project_import.rb | 2 +- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml create mode 100644 doc/api/project_import_export.md diff --git a/changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml b/changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml new file mode 100644 index 00000000000..29ab7cc7cab --- /dev/null +++ b/changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml @@ -0,0 +1,5 @@ +--- +title: API endpoint for importing a project export +merge_request: 17025 +author: +type: added diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md new file mode 100644 index 00000000000..c594db7d7f2 --- /dev/null +++ b/doc/api/project_import_export.md @@ -0,0 +1,72 @@ +# Project import API + +[Introduced][ce-41899] in GitLab 10.6 + +[Project import/export](../user/project/settings/import_export.md) + +## Import a file + +```http +POST /projects/import +``` + +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ---------------------------------------- | +| `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the user's namespace | +| `file` | string | yes | The file to be uploaded | +| `path` | string | yes | Name and path for new project | + +To upload a file from your filesystem, use the `--form` argument. This causes +cURL to post data using the header `Content-Type: multipart/form-data`. +The `file=` parameter must point to a file on your filesystem and be preceded +by `@`. For example: + +```console +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "path=api-project" -F "file=@/path/to/file" https://gitlab.example.com/api/v4/projects/import +``` + +```json +{ + "id": 1, + "description": null, + "name": "api-project", + "name_with_namespace": "Administrator / api-project", + "path": "api-project", + "path_with_namespace": "root/api-project", + "created_at": "2018-02-13T09:05:58.023Z", + "import_status": "scheduled" +} +``` + +## Import status + +Get the status of export. + +```http +GET /projects/:id/import +``` + +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ---------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | + +```console +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/import +``` + +Status can be one of `none`, `started`, or `finished`. + +```json +{ + "id": 1, + "description": "Itaque perspiciatis minima aspernatur corporis consequatur.", + "name": "Gitlab Test", + "name_with_namespace": "Gitlab Org / Gitlab Test", + "path": "gitlab-test", + "path_with_namespace": "gitlab-org/gitlab-test", + "created_at": "2017-08-29T04:36:44.383Z", + "import_status": "started" +} +``` + +[ce-41899]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41899 diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 1e51c92cbf1..e93c14ddcf5 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -19,8 +19,8 @@ module API resource :projects, requirements: { id: %r{[^/]+} } do params do requires :path, type: String, desc: 'The new project path and name' - optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' requires :file, type: File, desc: 'The project export file to be imported' + optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' end desc 'Get export status' do success Entities::ProjectImportStatus -- cgit v1.2.1 From 39122ea40dda9a1c5b353fc671219b51c625f6cb Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 15:12:18 +0100 Subject: update docs --- doc/api/project_import_export.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index c594db7d7f2..17b9b530be7 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -40,7 +40,7 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "path=api- ## Import status -Get the status of export. +Get the status of an import. ```http GET /projects/:id/import @@ -54,7 +54,9 @@ GET /projects/:id/import curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/import ``` -Status can be one of `none`, `started`, or `finished`. +Status can be one of `none`, `scheduled`, `failed`, `started`, or `finished`. + +If the status is `failed`, it will include the import error message. ```json { -- cgit v1.2.1 From e8813520029f0f07b0cf2d8463337ae09f15b7fe Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 15:18:52 +0100 Subject: refactor api class --- lib/api/project_import.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index e93c14ddcf5..b0511342b63 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -13,7 +13,7 @@ module API end before do - not_found! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project') + forbidden! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project') end resource :projects, requirements: { id: %r{[^/]+} } do @@ -22,12 +22,14 @@ module API requires :file, type: File, desc: 'The project export file to be imported' optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' end - desc 'Get export status' do + desc 'Create a new project import' do success Entities::ProjectImportStatus end post 'import' do render_api_error!('The file is invalid', 400) unless file_is_valid? + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437') + namespace = import_params[:namespace] namespace = if namespace.blank? current_user.namespace @@ -49,7 +51,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - desc 'Get export status' do + desc 'Get a project export status' do success Entities::ProjectImportStatus end get ':id/import' do -- cgit v1.2.1 From 083194908437f129adcb6e36be40af72ad6d308c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 13 Feb 2018 15:35:08 +0100 Subject: update missing doc links --- doc/api/projects.md | 4 ++++ doc/user/project/settings/import_export.md | 1 + 2 files changed, 5 insertions(+) diff --git a/doc/api/projects.md b/doc/api/projects.md index 46f5de5aa0e..08df65b2f66 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1326,6 +1326,10 @@ POST /projects/:id/housekeeping Read more in the [Branches](branches.md) documentation. +## Project Import/Export + +Read more in the [Project export](project_import_export.md) documentation. + ## Project members Read more in the [Project members](members.md) documentation. diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index b8f865679a2..dedf102fc37 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -22,6 +22,7 @@ > in the import side is required to map the users, based on email or username. > Otherwise, a supplementary comment is left to mention the original author and > the MRs, notes or issues will be owned by the importer. +> - Control project Import/Export with the [API](../../../api/project_import_export.md). Existing projects running on any GitLab instance or GitLab.com can be exported with all their related data and be moved into a new GitLab instance. -- cgit v1.2.1 From e613d777b258a4f7070d2b7aaee093901e4b7ed7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 14 Feb 2018 14:46:40 +0100 Subject: refactor code based on feedback --- lib/api/project_import.rb | 17 ++++++++++------- spec/requests/api/project_import_spec.rb | 28 ++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index b0511342b63..88fe1d2b5f5 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -10,19 +10,24 @@ module API def file_is_valid? import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read) end + + def validate_file! + render_api_error!('The file is invalid', 400) unless file_is_valid? + end end before do forbidden! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project') end - resource :projects, requirements: { id: %r{[^/]+} } do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do params do requires :path, type: String, desc: 'The new project path and name' requires :file, type: File, desc: 'The project export file to be imported' optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' end desc 'Create a new project import' do + detail 'This feature was introduced in GitLab 10.6.' success Entities::ProjectImportStatus end post 'import' do @@ -30,13 +35,10 @@ module API Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437') - namespace = import_params[:namespace] - namespace = if namespace.blank? - current_user.namespace - elsif namespace =~ /^\d+$/ - Namespace.find_by(id: namespace) + namespace = if import_params[:namespace] + find_namespace!(import_params[:namespace]) else - Namespace.find_by_path_or_name(namespace) + current_user.namespace end project_params = import_params.merge(namespace_id: namespace.id, @@ -52,6 +54,7 @@ module API requires :id, type: String, desc: 'The ID of a project' end desc 'Get a project export status' do + detail 'This feature was introduced in GitLab 10.6.' success Entities::ProjectImportStatus end get ':id/import' do diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 47c1edc7919..2fb103c6510 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -17,8 +17,7 @@ describe API::ProjectImport do describe 'POST /projects/import' do it 'schedules an import using a namespace' do - expect_any_instance_of(Project).to receive(:import_schedule) - expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(namespace.id, any_args).and_call_original + stub_import(user.namespace) post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.id @@ -26,8 +25,7 @@ describe API::ProjectImport do end it 'schedules an import using the namespace path' do - expect_any_instance_of(Project).to receive(:import_schedule) - expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(namespace.id, any_args).and_call_original + stub_import(unamespace) post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path @@ -35,14 +33,23 @@ describe API::ProjectImport do end it 'schedules an import at the user namespace level' do - expect_any_instance_of(Project).to receive(:import_schedule) - expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(user.namespace.id, any_args).and_call_original + stub_import(user.namespace) post api('/projects/import', user), path: 'test-import2', file: fixture_file_upload(file) expect(response).to have_gitlab_http_status(201) end + it 'schedules an import at the user namespace level' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + expect(Gitlab::ImportExport::ProjectCreator).not_to receive(:new) + + post api('/projects/import', user), namespace: 'nonexistent', path: 'test-import2', file: fixture_file_upload(file) + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Namespace Not Found') + end + it 'does not schedule an import if the user has no permission to the namespace' do expect_any_instance_of(Project).not_to receive(:import_schedule) @@ -51,8 +58,8 @@ describe API::ProjectImport do file: fixture_file_upload(file), namespace: namespace.full_path) - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Namespace is not valid') + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Namespace Not Found') end it 'does not schedule an import if the user uploads no valid file' do @@ -63,6 +70,11 @@ describe API::ProjectImport do expect(response).to have_gitlab_http_status(400) expect(json_response['error']).to eq('file is invalid') end + + def stub_import(namespace) + expect_any_instance_of(Project).to receive(:import_schedule) + expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(namespace.id, any_args).and_call_original + end end describe 'GET /projects/:id/import' do -- cgit v1.2.1 From 2dc2bad6233e1e74c74c49af4be838e0a39dcd53 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 14 Feb 2018 14:55:11 +0100 Subject: refactor code based on feedback --- doc/api/project_import_export.md | 2 +- spec/requests/api/project_import_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index 17b9b530be7..c473b94aafe 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -22,7 +22,7 @@ The `file=` parameter must point to a file on your filesystem and be preceded by `@`. For example: ```console -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "path=api-project" -F "file=@/path/to/file" https://gitlab.example.com/api/v4/projects/import +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "path=api-project" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/projects/import ``` ```json diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 2fb103c6510..eabf9095c13 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -17,7 +17,7 @@ describe API::ProjectImport do describe 'POST /projects/import' do it 'schedules an import using a namespace' do - stub_import(user.namespace) + stub_import(namespace) post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.id @@ -25,7 +25,7 @@ describe API::ProjectImport do end it 'schedules an import using the namespace path' do - stub_import(unamespace) + stub_import(namespace) post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path -- cgit v1.2.1 From 0abd85f919053efa8a03add9ae43ce9ea2d02ae5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 14 Feb 2018 14:59:11 +0100 Subject: refactor code based on feedback --- lib/api/project_import.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 88fe1d2b5f5..a6da9b90fe3 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -31,7 +31,7 @@ module API success Entities::ProjectImportStatus end post 'import' do - render_api_error!('The file is invalid', 400) unless file_is_valid? + validate_file! Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437') -- cgit v1.2.1 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: 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] ] -- cgit v1.2.1 From 4512ed80fc229a8048a950070fa2174754707bc0 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Sun, 28 Jan 2018 19:46:28 +0200 Subject: Fix new project path input overlapping - Max-width and height --- app/assets/stylesheets/framework/forms.scss | 2 +- app/assets/stylesheets/framework/selects.scss | 2 +- changelogs/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 -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From 9a450aedc2c81f8ce17e4c9b4238c58edbef8269 Mon Sep 17 00:00:00 2001 From: Turo Soisenniemi Date: Wed, 14 Feb 2018 19:41:13 +0000 Subject: Set Asciidoctor outfilesuffix to default .adoc Without setting outfilesuffix Asciidoctor defaults to .html. When .html is used, inter-document cross references do not work in Gitlab's repository / file renderer. See more information from http://asciidoctor.org/docs/user-manual/#navigating-between-source-files and http://asciidoctor.org/docs/user-manual/#inter-document-cross-references --- .../unreleased/asciidoc_inter_document_cross_references.yml | 5 +++++ lib/gitlab/asciidoc.rb | 3 ++- spec/lib/gitlab/asciidoc_spec.rb | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/asciidoc_inter_document_cross_references.yml diff --git a/changelogs/unreleased/asciidoc_inter_document_cross_references.yml b/changelogs/unreleased/asciidoc_inter_document_cross_references.yml new file mode 100644 index 00000000000..34b26753312 --- /dev/null +++ b/changelogs/unreleased/asciidoc_inter_document_cross_references.yml @@ -0,0 +1,5 @@ +--- +title: Asciidoc now support inter-document cross references between files in repository +merge_request: 17125 +author: Turo Soisenniemi +type: changed diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index ee7f4be6b9f..62c41801d75 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -8,7 +8,8 @@ module Gitlab module Asciidoc DEFAULT_ADOC_ATTRS = [ 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', - 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font' + 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font', + 'outfilesuffix=.adoc' ].freeze # Public: Converts the provided Asciidoc markup into HTML. diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index f668f78c2b8..2a0e19ae796 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -95,6 +95,14 @@ module Gitlab expect(render(input, context)).to include('

2+2 is 4

') end end + + context 'outfilesuffix' do + it 'defaults to adoc' do + output = render("Inter-document reference <>", context) + + expect(output).to include("a href=\"README.adoc\"") + end + end end def render(*args) -- cgit v1.2.1 From 6220c595c69b36be39e12f984b28c35ac97197e4 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Thu, 8 Feb 2018 14:03:20 +0200 Subject: 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 - .../unreleased/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 -- cgit v1.2.1 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: 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 ++- ...-blank-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 -- cgit v1.2.1 From 27a6d65c616b58fdce5d7eba7a11b7ef7b2c8d28 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Feb 2018 13:44:55 -0600 Subject: 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', -- cgit v1.2.1 From e5c8a47cbb39dddb0855c1ddfa505f083fff0c78 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 8 Feb 2018 15:36:11 -0600 Subject: 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; -- cgit v1.2.1 From b49d5dd11eda9e1213e0f3c87aab863924fe16cc Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 8 Feb 2018 15:39:51 -0600 Subject: add CHANGELOG.md entry for !17021 --- .../42641-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 -- cgit v1.2.1 From 1c2d283d506f2ceb4777622837fffe381e34cb9d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Feb 2018 16:50:20 -0600 Subject: Add dispatcher imports for bundles used in merge request show path --- .../javascripts/diff_notes/diff_notes_bundle.js | 4 ++-- app/assets/javascripts/how_to_merge.js | 25 +++++++++++----------- .../pages/projects/merge_requests/show/index.js | 8 +++++-- .../merge_requests/_how_to_merge.html.haml | 3 --- app/views/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]' -- cgit v1.2.1 From b866cba7f57025dad9e0c8dc93716d22baa87206 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 2 Feb 2018 15:50:09 -0600 Subject: Add Auto DevOps and Kubernetes cluster button to project page --- app/assets/stylesheets/framework/buttons.scss | 13 + app/assets/stylesheets/framework/mobile.scss | 4 - app/assets/stylesheets/framework/sidebar.scss | 1 - app/assets/stylesheets/framework/typography.scss | 2 +- app/assets/stylesheets/framework/variables.scss | 6 +- app/assets/stylesheets/pages/projects.scss | 53 ++-- app/helpers/projects_helper.rb | 214 ++++++++++++++ app/views/projects/_stat_anchor_list.html.haml | 5 + .../projects/_stat_anchor_list_item.html.haml | 7 + app/views/projects/empty.html.haml | 53 ++-- app/views/projects/show.html.haml | 60 +--- ...auto-devops-and-clusters-button-to-projects.yml | 6 + spec/features/auto_deploy_spec.rb | 77 ----- ...to_create_license_file_in_empty_project_spec.rb | 2 +- spec/features/projects/show_project_spec.rb | 317 +++++++++++++++++++++ spec/features/tags/master_views_tags_spec.rb | 2 +- 16 files changed, 627 insertions(+), 195 deletions(-) create mode 100644 app/views/projects/_stat_anchor_list.html.haml create mode 100644 app/views/projects/_stat_anchor_list_item.html.haml create mode 100644 changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml delete mode 100644 spec/features/auto_deploy_spec.rb diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index c4b046a6d68..6b89387ab5f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -444,6 +444,19 @@ } } +.btn-missing { + color: $notes-light-color; + border: 1px dashed $border-gray-normal-dashed; + border-radius: $border-radius-default; + + &:hover, + &:active, + &:focus { + color: $notes-light-color; + background-color: $white-normal; + } +} + .btn-svg svg { @include btn-svg; } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index a12f28efce6..8604e753c18 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -63,10 +63,6 @@ } } - .project-stats { - display: none; - } - .group-buttons { display: none; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d61809cb0a4..d1d98270ad9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -3,7 +3,6 @@ transition: padding $sidebar-transition-duration; .container-fluid { - background: $white-light; padding: 0 $gl-padding; &.container-blank { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index d0999e60e65..fef5a1f51fa 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -296,7 +296,7 @@ body { line-height: 1.3; font-size: 1.25em; font-weight: $gl-font-weight-bold; - margin: 12px 7px; + margin: 12px 0; } h1, diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 25ee081ea9c..945978d2f76 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -215,8 +215,8 @@ $tooltip-font-size: 12px; */ $gl-padding: 16px; $gl-padding-8: 8px; +$gl-padding-4: 4px; $gl-col-padding: 15px; -$gl-btn-padding: 10px; $gl-input-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; @@ -377,6 +377,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08); $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; $btn-white-active: #848484; +$gl-btn-padding: 10px; +$gl-btn-line-height: 16px; +$gl-btn-vert-padding: 8px; +$gl-btn-horz-padding: 12px; /* * Badges diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bf41005b6d5..93d232149c1 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -678,6 +678,9 @@ a.deploy-project-label { } } +.project-empty-note-panel { + border-bottom: 1px solid $border-color; +} .project-stats { font-size: 0; @@ -686,11 +689,13 @@ a.deploy-project-label { border-bottom: 1px solid $border-color; .nav { - padding-top: 12px; - padding-bottom: 12px; + margin-top: $gl-padding-8; + margin-bottom: $gl-padding-8; > li { display: inline-block; + margin-top: $gl-padding-4; + margin-bottom: $gl-padding-4; &:not(:last-child) { margin-right: $gl-padding; @@ -704,36 +709,32 @@ a.deploy-project-label { float: right; } } + } - > a { - padding: 0; - background-color: transparent; - font-size: 14px; - line-height: 29px; - color: $notes-light-color; + .stat-text, + .stat-link { + padding: $gl-btn-vert-padding 0; + background-color: transparent; + font-size: $gl-font-size; + line-height: $gl-btn-line-height; + color: $notes-light-color; + } - &:hover, - &:focus { - color: $gl-text-color; - text-decoration: underline; - } + .stat-link { + &:hover, + &:focus { + color: $gl-text-color; + text-decoration: underline; } } - } - - li.missing { - border: 1px dashed $border-gray-normal-dashed; - border-radius: $border-radius-default; - a { - padding-left: 10px; - padding-right: 10px; - color: $notes-light-color; - display: block; + .btn { + padding: $gl-btn-vert-padding $gl-btn-horz-padding; + line-height: $gl-btn-line-height; } - &:hover { - background-color: $gray-normal; + .btn-missing { + @extend .btn-missing; } } } @@ -743,7 +744,7 @@ pre.light-well { } .git-empty { - margin: 0 7px 7px; + margin-bottom: 7px; h5 { color: $gl-text-color; diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 6512617a02d..0c64b8abec3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -604,4 +604,218 @@ module ProjectsHelper project_find_file_path(@project, ref) end + + def can_current_user_push_code?(project) + project.empty_repo? ? can?(current_user, :push_code, project) : can_push_branch?(project, project.default_branch) + end + + def files_anchor_data(project) + { + enabled: true, + label: _('Files (%{human_size})') % { human_size: storage_counter(@project.statistics.total_repository_size) }, + link: project_tree_path(@project) + } + end + + def commits_anchor_data(project) + { + enabled: true, + label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', @project.statistics.commit_count) % { commit_count: number_with_delimiter(@project.statistics.commit_count) }, + link: project_commits_path(@project, current_ref) + } + end + + def branches_anchor_data(project) + { + enabled: true, + label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', @repository.branch_count) % { branch_count: number_with_delimiter(@repository.branch_count) }, + link: project_branches_path(@project) + } + end + + def tags_anchor_data(project) + { + enabled: true, + label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', @repository.tag_count) % { tag_count: number_with_delimiter(@repository.tag_count) }, + link: project_tags_path(@project) + } + end + + def new_file_anchor_data(project) + if current_user && can_current_user_push_code?(project) + { + enabled: false, + label: _('New file'), + link: project_new_blob_path(project, project.default_branch || 'master'), + class_modifier: 'new' + } + end + end + + def readme_anchor_data(project) + if current_user && can_current_user_push_code?(project) && project.repository.readme.blank? + { + enabled: false, + label: _('Add Readme'), + link: add_special_file_path(project, file_name: 'README.md') + } + elsif project.repository.readme.present? + { + enabled: true, + label: _('Readme'), + link: default_project_view != 'readme' ? readme_path(@project) : '#readme' + } + end + end + + def changelog_anchor_data(project) + if current_user && can_current_user_push_code?(project) && project.repository.changelog.blank? + { + enabled: false, + label: _('Add Changelog'), + link: add_special_file_path(project, file_name: 'CHANGELOG') + } + elsif project.repository.changelog.present? + { + enabled: true, + label: _('Changelog'), + link: changelog_path(project) + } + end + end + + def license_anchor_data(project) + if current_user && can_current_user_push_code?(project) && project.repository.license_blob.blank? + { + enabled: false, + label: _('Add License'), + link: add_special_file_path(project, file_name: 'LICENSE') + } + elsif project.repository.license_blob.present? + { + enabled: true, + label: license_short_name(project), + link: license_path(project) + } + end + end + + def contribution_guide_anchor_data(project) + if current_user && can_current_user_push_code?(project) && project.repository.contribution_guide.blank? + { + enabled: false, + label: _('Add Contribution guide'), + link: add_special_file_path(project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') + } + elsif project.repository.contribution_guide.present? + { + enabled: true, + label: _('Contribution guide'), + link: contribution_guide_path(@project) + } + end + end + + def autodevops_anchor_data(project, ignore_callout: false) + if current_user && can?(current_user, :admin_pipeline, project) && project.repository.gitlab_ci_yml.blank? && (ignore_callout || !show_auto_devops_callout?(project)) + { + enabled: project.auto_devops_enabled?, + label: project.auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), + link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings') + } + elsif project.auto_devops_enabled? + { + enabled: true, + label: _('Auto DevOps enabled'), + link: nil + } + end + end + + def kubernetes_cluster_anchor_data(project) + if current_user && can?(current_user, :create_cluster, project) + cluster_link = project.clusters.size == 1 ? project_cluster_path(project, project.clusters.first) : project_clusters_path(project) + + if project.clusters.empty? + cluster_link = new_project_cluster_path(project) + end + + { + enabled: !project.clusters.empty?, + label: project.clusters.empty? ? _('Add Kubernetes cluster') : n_('Kubernetes cluster', 'Kubernetes clusters', project.clusters.size), + link: cluster_link + } + end + end + + def gitlab_ci_anchor_data(project) + if current_user && can_current_user_push_code?(project) && project.repository.gitlab_ci_yml.blank? && !project.auto_devops_enabled? + { + enabled: false, + label: _('Set up CI/CD'), + link: add_special_file_path(project, file_name: '.gitlab-ci.yml') + } + elsif project.repository.gitlab_ci_yml.present? + { + enabled: true, + label: _('CI/CD configuration'), + link: ci_configuration_path(@project) + } + end + end + + def koding_anchor_data(project) + if current_user && can_current_user_push_code?(project) && koding_enabled? && project.repository.koding_yml.blank? + { + enabled: false, + label: _('Set up Koding'), + link: add_koding_stack_path(project) + } + end + end + + def empty_project_stat_anchor_items(project) + [ + autodevops_anchor_data(project, ignore_callout: true), + kubernetes_cluster_anchor_data(project) + ].compact.reject { |i| !i[:enabled] } + end + + def empty_project_stat_button_items(project) + [ + new_file_anchor_data(project), + readme_anchor_data(project), + license_anchor_data(project), + autodevops_anchor_data(project, ignore_callout: true), + kubernetes_cluster_anchor_data(project) + ].compact.reject { |i| i[:enabled] } + end + + def project_stat_anchor_items(project) + [ + files_anchor_data(project), + commits_anchor_data(project), + branches_anchor_data(project), + tags_anchor_data(project), + readme_anchor_data(project), + changelog_anchor_data(project), + license_anchor_data(project), + contribution_guide_anchor_data(project), + gitlab_ci_anchor_data(project), + autodevops_anchor_data(project), + kubernetes_cluster_anchor_data(project) + ].compact.reject { |i| !i[:enabled] } + end + + def project_stat_button_items(project) + [ + changelog_anchor_data(project), + license_anchor_data(project), + contribution_guide_anchor_data(project), + autodevops_anchor_data(project), + kubernetes_cluster_anchor_data(project), + gitlab_ci_anchor_data(project), + koding_anchor_data(project) + ].compact.reject { |i| i[:enabled] } + end end diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml new file mode 100644 index 00000000000..9ca51f8d68c --- /dev/null +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -0,0 +1,5 @@ +- anchors = local_assigns.fetch(:anchors, []) + +- if anchors.size > 0 + %ul.nav + = render partial: 'stat_anchor_list_item', collection: anchors, as: :anchor diff --git a/app/views/projects/_stat_anchor_list_item.html.haml b/app/views/projects/_stat_anchor_list_item.html.haml new file mode 100644 index 00000000000..404dd24599f --- /dev/null +++ b/app/views/projects/_stat_anchor_list_item.html.haml @@ -0,0 +1,7 @@ +%li + - if anchor[:link] + = link_to anchor[:link], class: anchor[:enabled] ? 'stat-link' : "btn btn-#{anchor[:class_modifier] || 'missing'}" do + = anchor[:label] + - else + %span.stat-text + = anchor[:label] diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index ab225796b12..01fb9dab313 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -5,38 +5,41 @@ = render "home_panel" -.row-content-block.second-block.center - %h4 - The repository for this project is empty +.project-empty-note-panel + %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } + .prepend-top-20 + %h4 + = _('The repository for this project is empty') + + - if can?(current_user, :push_code, @project) + %p + - link_to_cli = link_to _('command line instructions'), '#repo-command-line-instructions' + = _('If you already have files you can push them using the %{link_to_cli} below.').html_safe % { link_to_cli: link_to_cli } + %p + %em + - link_to_protected_branches = link_to _('Learn more about protected branches'), help_page_path('user/project/protected_branches') + = _('Note that the master branch is automatically protected. %{link_to_protected_branches}').html_safe % { link_to_protected_branches: link_to_protected_branches } - - if can?(current_user, :push_code, @project) - %p - If you already have files you can push them using command line instructions below. - %p - Otherwise you can start with adding a - = succeed ',' do - = link_to "README", add_special_file_path(@project, file_name: 'README.md') - a - = succeed ',' do - = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE') - or a - = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore') - to this project. - %p - You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected. + %hr + %p + - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) + - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), project_clusters_path(@project)) + = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } - - if show_auto_devops_callout?(@project) + %hr %p - - link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) - = s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link } - %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') - %p= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master'), class: 'btn btn-new' + = _('Otherwise it is recommended you start with one of the options below.') + .prepend-top-20 + +%nav.project-stats{ class: container_class } + = render 'stat_anchor_list', anchors: empty_project_stat_anchor_items(@project) + = render 'stat_anchor_list', anchors: empty_project_stat_button_items(@project) - if can?(current_user, :push_code, @project) - %div{ class: container_class } + %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } .prepend-top-20 .empty_wrapper - %h3.page-title-empty + %h3#repo-command-line-instructions.page-title-empty Command line instructions .git-empty %fieldset diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 888d820b04e..3d23b19b815 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -14,65 +14,9 @@ - if can?(current_user, :download_code, @project) %nav.project-stats{ class: container_class } - %ul.nav - %li - = link_to project_tree_path(@project) do - #{_('Files')} (#{storage_counter(@project.statistics.total_repository_size)}) - %li - = link_to project_commits_path(@project, current_ref) do - #{n_('Commit', 'Commits', @project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %li - = link_to project_branches_path(@project) do - #{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) - %li - = link_to project_tags_path(@project) do - #{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) + = render 'stat_anchor_list', anchors: project_stat_anchor_items(@project) + = render 'stat_anchor_list', anchors: project_stat_button_items(@project) - - if @repository.readme - %li - = link_to _('Readme'), - default_project_view != 'readme' ? readme_path(@project) : '#readme' - - - if @repository.changelog - %li - = link_to _('Changelog'), changelog_path(@project) - - - if @repository.license_blob - %li - = link_to license_short_name(@project), license_path(@project) - - - if @repository.contribution_guide - %li - = link_to _('Contribution guide'), contribution_guide_path(@project) - - - if @repository.gitlab_ci_yml - %li - = link_to _('CI/CD configuration'), ci_configuration_path(@project) - - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - #{ _('Add Changelog') } - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - #{ _('Add License') } - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - #{ _('Add Contribution guide') } - - unless @repository.gitlab_ci_yml - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - #{ _('Set up CI/CD') } - - if koding_enabled? && @repository.koding_yml.blank? - %li.missing - = link_to _('Set up Koding'), add_koding_stack_path(@project) - - if @repository.gitlab_ci_yml.blank? && @project.deployment_platform.present? - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do - #{ _('Set up auto deploy') } %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - if @project.archived? diff --git a/changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml b/changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml new file mode 100644 index 00000000000..5613b2af763 --- /dev/null +++ b/changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml @@ -0,0 +1,6 @@ +--- +title: Add a button on the project page to set up a Kubernetes cluster and enable + Auto DevOps +merge_request: 16900 +author: +type: added diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb deleted file mode 100644 index 9aef68b7156..00000000000 --- a/spec/features/auto_deploy_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe 'Auto deploy' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do - context 'when no deployment service is active' do - before do - trun_off - end - - it 'does not show a button to set up auto deploy' do - visit project_path(project) - expect(page).to have_no_content('Set up auto deploy') - end - end - - context 'when a deployment service is active' do - before do - trun_on - visit project_path(project) - end - - it 'shows a button to set up auto deploy' do - expect(page).to have_link('Set up auto deploy') - end - - it 'includes OpenShift as an available template', :js do - click_link 'Set up auto deploy' - click_button 'Apply a GitLab CI Yaml template' - - within '.gitlab-ci-yml-selector' do - expect(page).to have_content('OpenShift') - end - end - - it 'creates a merge request using "auto-deploy" branch', :js do - click_link 'Set up auto deploy' - click_button 'Apply a GitLab CI Yaml template' - within '.gitlab-ci-yml-selector' do - click_on 'OpenShift' - end - wait_for_requests - click_button 'Commit changes' - - expect(page).to have_content('New Merge Request From auto-deploy into master') - end - end - end - - context 'when user configured kubernetes from Integration > Kubernetes' do - before do - create :kubernetes_service, project: project - project.add_master(user) - sign_in user - end - - let(:trun_on) { project.deployment_platform.update!(active: true) } - let(:trun_off) { project.deployment_platform.update!(active: false) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - before do - create(:cluster, :provided_by_gcp, projects: [project]) - project.add_master(user) - sign_in user - end - - let(:trun_on) { project.deployment_platform.cluster.update!(enabled: true) } - let(:trun_off) { project.deployment_platform.cluster.update!(enabled: false) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end -end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 8ac9821b879..7f1d1934103 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -11,7 +11,7 @@ feature 'project owner sees a link to create a license file in empty project', : scenario 'project master creates a license file from a template' do visit project_path(project) - click_on 'LICENSE' + click_on 'Add License' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb index 0b94c9eae5d..f5f2bbb49e0 100644 --- a/spec/features/projects/show_project_spec.rb +++ b/spec/features/projects/show_project_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Project show page', :feature do + include ProjectsHelper + context 'when project pending delete' do let(:project) { create(:project, :empty_repo, pending_delete: true) } @@ -17,4 +19,319 @@ describe 'Project show page', :feature do expect(page).to have_content("This project was scheduled for deletion, but failed with the following message: #{project.delete_error}") end end + + describe 'stat button existence' do + # For "New file", "Add License" functionality, + # see spec/features/projects/files/project_owner_creates_license_file_spec.rb + # see spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb + + let(:user) { create(:user) } + + describe 'empty project' do + let(:project) { create(:project, :public, :empty_repo) } + + describe 'as a normal user' do + before do + sign_in(user) + + visit project_path(project) + end + + it 'no Auto DevOps button if can not manage pipelines' do + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + + it '"Auto DevOps enabled" button not linked' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_text('Auto DevOps enabled') + end + end + end + + describe 'as a master' do + before do + project.add_master(user) + sign_in(user) + + visit project_path(project) + end + + it '"New file" button linked to new file page' do + page.within('.project-stats') do + expect(page).to have_link('New file', href: project_new_blob_path(project, project.default_branch || 'master')) + end + end + + it '"Add Readme" button linked to new file populated for a readme' do + page.within('.project-stats') do + expect(page).to have_link('Add Readme', href: add_special_file_path(project, file_name: 'README.md')) + end + end + + it '"Add License" button linked to new file populated for a license' do + page.within('.project-stats') do + expect(page).to have_link('Add License', href: add_special_file_path(project, file_name: 'LICENSE')) + end + end + + describe 'Auto DevOps button' do + it '"Enable Auto DevOps" button linked to settings page' do + page.within('.project-stats') do + expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + + it '"Auto DevOps enabled" anchor linked to settings page' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + end + + describe 'Kubernetes cluster button' do + it '"Add Kubernetes cluster" button linked to clusters page' do + page.within('.project-stats') do + expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project)) + end + end + + it '"Kubernetes cluster" anchor linked to cluster page' do + cluster = create(:cluster, :provided_by_gcp, projects: [project]) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Kubernetes cluster', href: project_cluster_path(project, cluster)) + end + end + end + end + end + + describe 'populated project' do + let(:project) { create(:project, :public, :repository) } + + describe 'as a normal user' do + before do + sign_in(user) + + visit project_path(project) + end + + it 'no Auto DevOps button if can not manage pipelines' do + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + + it '"Auto DevOps enabled" button not linked' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_text('Auto DevOps enabled') + end + end + + it 'no Kubernetes cluster button if can not manage clusters' do + page.within('.project-stats') do + expect(page).not_to have_link('Add Kubernetes cluster') + expect(page).not_to have_link('Kubernetes cluster') + end + end + end + + describe 'as a master' do + before do + allow_any_instance_of(AutoDevopsHelper).to receive(:show_auto_devops_callout?).and_return(false) + project.add_master(user) + sign_in(user) + + visit project_path(project) + end + + it 'no "Add Changelog" button if the project already has a changelog' do + expect(project.repository.changelog).not_to be_nil + + page.within('.project-stats') do + expect(page).not_to have_link('Add Changelog') + end + end + + it 'no "Add License" button if the project already has a license' do + expect(project.repository.license_blob).not_to be_nil + + page.within('.project-stats') do + expect(page).not_to have_link('Add License') + end + end + + it 'no "Add Contribution guide" button if the project already has a contribution guide' do + expect(project.repository.contribution_guide).not_to be_nil + + page.within('.project-stats') do + expect(page).not_to have_link('Add Contribution guide') + end + end + + describe 'GitLab CI configuration button' do + it '"Set up CI/CD" button linked to new file populated for a .gitlab-ci.yml' do + expect(project.repository.gitlab_ci_yml).to be_nil + + page.within('.project-stats') do + expect(page).to have_link('Set up CI/CD', href: add_special_file_path(project, file_name: '.gitlab-ci.yml')) + end + end + + it 'no "Set up CI/CD" button if the project already has a .gitlab-ci.yml' do + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab-ci.yml", + file_path: '.gitlab-ci.yml', + file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + ).execute + + expect(project.repository.gitlab_ci_yml).not_to be_nil + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up CI/CD') + end + end + + it 'no "Set up CI/CD" button if the project has Auto DevOps enabled' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up CI/CD') + end + end + end + + describe 'Auto DevOps button' do + it '"Enable Auto DevOps" button linked to settings page' do + page.within('.project-stats') do + expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + + it '"Enable Auto DevOps" button linked to settings page' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + + it 'no Auto DevOps button if Auto DevOps callout is shown' do + allow_any_instance_of(AutoDevopsHelper).to receive(:show_auto_devops_callout?).and_return(true) + + visit project_path(project) + + expect(page).to have_selector('.js-autodevops-banner') + + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + + it 'no "Enable Auto DevOps" button when .gitlab-ci.yml already exists' do + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab-ci.yml", + file_path: '.gitlab-ci.yml', + file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + ).execute + + expect(project.repository.gitlab_ci_yml).not_to be_nil + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + end + + describe 'Kubernetes cluster button' do + it '"Add Kubernetes cluster" button linked to clusters page' do + page.within('.project-stats') do + expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project)) + end + end + + it '"Kubernetes cluster" button linked to cluster page' do + cluster = create(:cluster, :provided_by_gcp, projects: [project]) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Kubernetes cluster', href: project_cluster_path(project, cluster)) + end + end + end + + describe '"Set up Koding" button' do + it 'no "Set up Koding" button if Koding disabled' do + stub_application_setting(koding_enabled?: false) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up Koding') + end + end + + it 'no "Set up Koding" button if the project already has a .koding.yml' do + stub_application_setting(koding_enabled?: true) + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:koding_url).and_return('http://koding.example.com') + expect(project.repository.changelog).not_to be_nil + allow_any_instance_of(Repository).to receive(:koding_yml).and_return(project.repository.changelog) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up Koding') + end + end + + it '"Set up Koding" button linked to new file populated for a .koding.yml' do + stub_application_setting(koding_enabled?: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Set up Koding', href: add_koding_stack_path(project)) + end + end + end + end + end + end end diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb index 4662367d843..b625e7065cc 100644 --- a/spec/features/tags/master_views_tags_spec.rb +++ b/spec/features/tags/master_views_tags_spec.rb @@ -13,7 +13,7 @@ feature 'Master views tags' do before do visit project_path(project) - click_on 'README' + click_on 'Add Readme' fill_in :commit_message, with: 'Add a README file', visible: true click_button 'Commit changes' visit project_tags_path(project) -- cgit v1.2.1 From 092c56018a4357776aad1192bfb542af3eedcde0 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Fri, 16 Feb 2018 15:20:16 +0500 Subject: 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 -- cgit v1.2.1 From a69b36264b91223934e23184175ad8f9622465fa Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 16 Feb 2018 12:00:13 +0100 Subject: Fix and simplify end-to-end tests for secret variables --- qa/qa/factory/resource/secret_variable.rb | 12 ---------- qa/qa/page/base.rb | 4 ++++ qa/qa/page/project/settings/secret_variables.rb | 26 ++++++++++------------ .../features/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 -- cgit v1.2.1 From 8469725ec4a43b24f825017c5447529f9b4a2cc8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 16 Feb 2018 11:10:34 +0000 Subject: 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 -- cgit v1.2.1 From 523f1f7cf484bea334c365ca21ab3dd492613b55 Mon Sep 17 00:00:00 2001 From: Constance Okoghenun Date: Fri, 16 Feb 2018 12:38:50 +0100 Subject: Removed prometheus_metrics webpack bundle tag --- app/assets/javascripts/pages/projects/services/edit/index.js | 7 +++++++ app/assets/javascripts/prometheus_metrics/index.js | 6 ------ app/views/projects/services/prometheus/_show.html.haml | 3 --- config/webpack.config.js | 1 - 4 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 app/assets/javascripts/prometheus_metrics/index.js diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js index 434a7e44277..5c73171e62e 100644 --- a/app/assets/javascripts/pages/projects/services/edit/index.js +++ b/app/assets/javascripts/pages/projects/services/edit/index.js @@ -1,6 +1,13 @@ import IntegrationSettingsForm from '~/integrations/integration_settings_form'; +import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; export default () => { + const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring'); const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); integrationSettingsForm.init(); + + if (prometheusSettingsWrapper) { + const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + prometheusMetrics.loadActiveMetrics(); + } }; diff --git a/app/assets/javascripts/prometheus_metrics/index.js b/app/assets/javascripts/prometheus_metrics/index.js deleted file mode 100644 index a0c43c5abe1..00000000000 --- a/app/assets/javascripts/prometheus_metrics/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import PrometheusMetrics from './prometheus_metrics'; - -$(() => { - const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); - prometheusMetrics.loadActiveMetrics(); -}); diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index b0cb5ce5e8f..5f38ecd6820 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -1,6 +1,3 @@ -- content_for :page_specific_javascripts do - = webpack_bundle_tag('prometheus_metrics') - .row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring .col-lg-3 %h4.prepend-top-0 diff --git a/config/webpack.config.js b/config/webpack.config.js index 3c581a156dd..4e0ae90f8ef 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -83,7 +83,6 @@ var config = { pipelines_times: './pipelines/pipelines_times.js', profile: './profile/profile_bundle.js', project_import_gl: './projects/project_import_gitlab_project.js', - prometheus_metrics: './prometheus_metrics', protected_branches: './protected_branches', protected_tags: './protected_tags', registry_list: './registry/index.js', -- cgit v1.2.1 From 2b64c67c1f563f805e53b14777452382b0c7e04a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 16 Feb 2018 13:13:59 +0100 Subject: 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 -- cgit v1.2.1 From 2f817f6528db0b835bd59f3241e2cf72b83cdb3a Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 13 Feb 2018 14:32:34 +0100 Subject: Replace "Kubernetes cluster" where appropriate in docs Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/42939 --- doc/ci/README.md | 2 + doc/user/project/clusters/index.md | 117 +++++++++++++++++-------------------- 2 files changed, 55 insertions(+), 64 deletions(-) diff --git a/doc/ci/README.md b/doc/ci/README.md index eabeb4510db..532ae52a184 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -70,6 +70,8 @@ learn how to leverage its potential even more. - [Use SSH keys in your build environment](ssh_keys/README.md) - [Trigger pipelines through the GitLab API](triggers/README.md) - [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md) +- [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or + more Kubernetes clusters to your project ## GitLab CI/CD for Docker diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 50a8e0d5ec5..bbe25c2d911 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -5,20 +5,23 @@ Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster in a few steps. -With a cluster associated to your project, you can use Review Apps, deploy your -applications, run your pipelines, and much more, in an easy way. +## Overview + +With a Kubernetes cluster associated to your project, you can use +[Review Apps](../../../ci/review_apps/index.md), deploy your applications, run +your pipelines, and much more, in an easy way. There are two options when adding a new cluster to your project; either associate your account with Google Kubernetes Engine (GKE) so that you can [create new clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab, or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster). -## Prerequisites +## Adding and creating a new GKE cluster via GitLab -In order to be able to manage your Kubernetes cluster through GitLab, the -following prerequisites must be met. +NOTE: **Note:** +You need Master [permissions] and above to access the Kubernetes page. -**For a cluster hosted on GKE:** +Before proceeding, make sure the following requirements are met: - The [Google authentication integration](../../../integration/google.md) must be enabled in GitLab at the instance level. If that's not the case, ask your @@ -28,30 +31,16 @@ following prerequisites must be met. account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) must be set up and that you have to have permissions to access it. - You must have Master [permissions] in order to be able to access the - **Cluster** page. + **Kubernetes** page. - You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled - You must have [Resource Manager API](https://cloud.google.com/resource-manager/) -**For an existing Kubernetes cluster:** - -- Since the cluster is already created, there are no prerequisites. - ---- - -If all of the above requirements are met, you can proceed to add a new Kubernetes -cluster. - -## Adding and creating a new GKE cluster via GitLab - -NOTE: **Note:** -You need Master [permissions] and above to access the Clusters page. - -Before proceeding, make sure all [prerequisites](#prerequisites) are met. -To add a new cluster hosted on GKE to your project: +If all of the above requirements are met, you can proceed to create and add a +new Kubernetes cluster that will be hosted on GKE to your project: -1. Navigate to your project's **CI/CD > Clusters** page. -1. Click on **Add cluster**. +1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Click on **Add Kubernetes cluster**. 1. Click on **Create with GKE**. 1. Connect your Google account if you haven't done already by clicking the **Sign in with Google** button. @@ -66,7 +55,7 @@ To add a new cluster hosted on GKE to your project: - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) of the Virtual Machine instance that the cluster will be based on. - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. -1. Finally, click the **Create cluster** button. +1. Finally, click the **Create Kubernetes cluster** button. After a few moments, your cluster should be created. If something goes wrong, you will be notified. @@ -77,14 +66,14 @@ enable the Cluster integration. ## Adding an existing Kubernetes cluster NOTE: **Note:** -You need Master [permissions] and above to access the Clusters page. +You need Master [permissions] and above to access the Kubernetes page. To add an existing Kubernetes cluster to your project: -1. Navigate to your project's **CI/CD > Clusters** page. -1. Click on **Add cluster**. -1. Click on **Add an existing cluster** and fill in the details: - - **Cluster name** (required) - The name you wish to give the cluster. +1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Click on **Add Kuberntes cluster**. +1. Click on **Add an existing Kubernetes cluster** and fill in the details: + - **Kubernetes cluster name** (required) - The name you wish to give the cluster. - **Environment scope** (required)- The [associated environment](#setting-the-environment-scope) to this cluster. - **API URL** (required) - @@ -112,15 +101,13 @@ To add an existing Kubernetes cluster to your project: - If you or someone created a secret specifically for the project, usually with limited permissions, the secret's namespace and project namespace may be the same. -1. Finally, click the **Create cluster** button. - -The Kubernetes service takes the following parameters: +1. Finally, click the **Create Kuberntes cluster** button. After a few moments, your cluster should be created. If something goes wrong, you will be notified. You can now proceed to install some pre-defined applications and then -enable the Cluster integration. +enable the Kubernetes cluster integration. ## Installing applications @@ -139,7 +126,7 @@ added directly to your configured cluster. Those applications are needed for NOTE: **Note:** You need a load balancer installed in your cluster in order to obtain the external IP address with the following procedure. It can be deployed using the -**Ingress** application described in the previous section. +[**Ingress** application](#installing-appplications). In order to publish your web application, you first need to find the external IP address associated to your load balancer. @@ -153,7 +140,8 @@ the `gcloud` command in a local terminal or using the **Cloud Shell**. If the cluster is not on GKE, follow the specific instructions for your Kubernetes provider to configure `kubectl` with the right credentials. -If you installed the Ingress using the **Applications** section, run the following command: +If you installed the Ingress [via the **Applications**](#installing-applications), +run the following command: ```bash kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' @@ -171,9 +159,14 @@ your deployed applications. ## Setting the environment scope -When adding more than one clusters, you need to differentiate them with an -environment scope. The environment scope associates clusters and -[environments](../../../ci/environments.md) in an 1:1 relationship similar to how the +NOTE: **Note:** +This is only available for [GitLab Premium][ee] where you can add more than +one Kubernetes cluster. + +When adding more than one Kubernetes clusters to your project, you need to +differentiate them with an environment scope. The environment scope associates +clusters and [environments](../../../ci/environments.md) in an 1:1 relationship +similar to how the [environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables) work. @@ -183,7 +176,7 @@ cluster in a project, and a validation error will occur if otherwise. --- -For example, let's say the following clusters exist in a project: +For example, let's say the following Kubernetes clusters exist in a project: | Cluster | Environment scope | | ---------- | ------------------- | @@ -231,8 +224,7 @@ With GitLab Premium, you can associate more than one Kubernetes clusters to your project. That way you can have different clusters for different environments, like dev, staging, production, etc. -To add another cluster, follow the same steps as described in [adding a -Kubernetes cluster](#adding-a-kubernetes-cluster) and make sure to +Simply add another cluster, like you did the first time, and make sure to [set an environment scope](#setting-the-environment-scope) that will differentiate the new cluster with the rest. @@ -240,45 +232,42 @@ differentiate the new cluster with the rest. The Kubernetes cluster integration exposes the following [deployment variables](../../../ci/variables/README.md#deployment-variables) in the -GitLab CI/CD build environment: - -- `KUBE_URL` - Equal to the API URL. -- `KUBE_TOKEN` - The Kubernetes token. -- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified. - The default value is `-`. You can overwrite it to - use different one if needed, otherwise the `KUBE_NAMESPACE` variable will - receive the default value. -- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path - to a file containing PEM data. -- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data. -- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment. - CA bundle would be embedded if specified. - -## Enabling or disabling the Cluster integration +GitLab CI/CD build environment. + +| Variable | Description | +| -------- | ----------- | +| `KUBE_URL` | Equal to the API URL. | +| `KUBE_TOKEN` | The Kubernetes token. | +| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `-`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. | +| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. | +| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. | +| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. | + +## Enabling or disabling the Kubernetes cluster integration After you have successfully added your cluster information, you can enable the -Cluster integration: +Kubernetes cluster integration: 1. Click the "Enabled/Disabled" switch 1. Hit **Save** for the changes to take effect You can now start using your Kubernetes cluster for your deployments. -To disable the Cluster integration, follow the same procedure. +To disable the Kubernetes cluster integration, follow the same procedure. -## Removing the Cluster integration +## Removing the Kubernetes cluster integration NOTE: **Note:** -You need Master [permissions] and above to remove a cluster integration. +You need Master [permissions] and above to remove a Kubernetes cluster integration. NOTE: **Note:** When you remove a cluster, you only remove its relation to GitLab, not the cluster itself. To remove the cluster, you can do so by visiting the GKE dashboard or using `kubectl`. -To remove the Cluster integration from your project, simply click on the +To remove the Kubernetes cluster integration from your project, simply click on the **Remove integration** button. You will then be able to follow the procedure -and [add a cluster](#adding-a-cluster) again. +and add a Kubernetes cluster again. ## What you can get with the Kubernetes integration -- cgit v1.2.1 From 890d7b540b1ffbadcde490a2e1b741bbb1af3cf4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 16 Feb 2018 14:37:26 +0100 Subject: update docs --- doc/api/README.md | 1 + doc/api/project_import_export.md | 6 +++--- doc/api/projects.md | 2 +- lib/api/project_import.rb | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/api/README.md b/doc/api/README.md index f226716c3b5..1927489b7bb 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -43,6 +43,7 @@ following locations: - [Pipeline Schedules](pipeline_schedules.md) - [Projects](projects.md) including setting Webhooks - [Project Access Requests](access_requests.md) +- [Project import/export](project_import_export.md) - [Project Members](members.md) - [Project Snippets](project_snippets.md) - [Protected Branches](protected_branches.md) diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index c473b94aafe..e442442c750 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -2,7 +2,7 @@ [Introduced][ce-41899] in GitLab 10.6 -[Project import/export](../user/project/settings/import_export.md) +[See also the project import/export documentation](../user/project/settings/import_export.md) ## Import a file @@ -12,7 +12,7 @@ POST /projects/import | Attribute | Type | Required | Description | | --------- | -------------- | -------- | ---------------------------------------- | -| `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the user's namespace | +| `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace | | `file` | string | yes | The file to be uploaded | | `path` | string | yes | Name and path for new project | @@ -56,7 +56,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a Status can be one of `none`, `scheduled`, `failed`, `started`, or `finished`. -If the status is `failed`, it will include the import error message. +If the status is `failed`, it will include the import error message under `import_error`. ```json { diff --git a/doc/api/projects.md b/doc/api/projects.md index 08df65b2f66..2cdfd1d2ca7 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1328,7 +1328,7 @@ Read more in the [Branches](branches.md) documentation. ## Project Import/Export -Read more in the [Project export](project_import_export.md) documentation. +Read more in the [Project import/export](project_import_export.md) documentation. ## Project members diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index a6da9b90fe3..c32e2f26ae3 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -24,7 +24,7 @@ module API params do requires :path, type: String, desc: 'The new project path and name' requires :file, type: File, desc: 'The project export file to be imported' - optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be imported into. Defaults to the user namespace.' + optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace." end desc 'Create a new project import' do detail 'This feature was introduced in GitLab 10.6.' -- cgit v1.2.1 From e19d45ddcfefe13343312ba4bd8ad1c1563394ba Mon Sep 17 00:00:00 2001 From: Olivier Gonzalez Date: Fri, 16 Feb 2018 09:09:24 -0500 Subject: Update vendored AutoDevOps YML for latest SAST changes --- vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 094d6791505..0ac8885405e 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -321,7 +321,15 @@ production: # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" SAST_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - docker run --volume "$PWD:/code" \ + # Deprecation notice for CONFIDENCE_LEVEL variable + if [ -z "$SAST_CONFIDENCE_LEVEL" -a "$CONFIDENCE_LEVEL" ]; then + SAST_CONFIDENCE_LEVEL="$CONFIDENCE_LEVEL" + echo "WARNING: CONFIDENCE_LEVEL is deprecated and MUST be replaced with SAST_CONFIDENCE_LEVEL" + fi + + docker run --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}" \ + --env SAST_DISABLE_REMOTE_CHECKS="${SAST_DISABLE_REMOTE_CHECKS:-false}" \ + --volume "$PWD:/code" \ --volume /var/run/docker.sock:/var/run/docker.sock \ "registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code ;; -- cgit v1.2.1 From b94131b6704a3240832a638844e9a07d1fbe93a3 Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Fri, 16 Feb 2018 15:47:34 +0000 Subject: Update GCP install instructions to utilize a standard VM --- .../google_cloud_platform/img/boot_disk.png | Bin 0 -> 124175 bytes .../img/change_admin_passwd_email.png | Bin 7193 -> 0 bytes .../img/chrome_not_secure_page.png | Bin 21705 -> 0 bytes .../google_cloud_platform/img/first_signin.png | Bin 0 -> 69516 bytes .../img/gcp_gitlab_being_deployed.png | Bin 23486 -> 0 bytes .../img/gcp_gitlab_overview.png | Bin 42028 -> 0 bytes .../google_cloud_platform/img/gcp_landing.png | Bin 59912 -> 15025 bytes .../img/gcp_launcher_console_home_page.png | Bin 42090 -> 0 bytes .../img/gcp_search_for_gitlab.png | Bin 7648 -> 0 bytes .../img/gitlab_deployed_page.png | Bin 35573 -> 0 bytes .../img/gitlab_first_sign_in.png | Bin 20054 -> 0 bytes .../img/gitlab_launch_button.png | Bin 5198 -> 0 bytes .../google_cloud_platform/img/launch_vm.png | Bin 0 -> 47201 bytes .../img/new_gitlab_deployment_settings.png | Bin 50014 -> 0 bytes .../google_cloud_platform/img/ssh_terminal.png | Bin 0 -> 94161 bytes .../google_cloud_platform/img/ssh_via_button.png | Bin 3062 -> 0 bytes .../google_cloud_platform/img/vm_created.png | Bin 0 -> 13809 bytes .../google_cloud_platform/img/vm_details.png | Bin 0 -> 76519 bytes doc/install/google_cloud_platform/index.md | 86 ++++++--------------- 19 files changed, 24 insertions(+), 62 deletions(-) create mode 100644 doc/install/google_cloud_platform/img/boot_disk.png delete mode 100644 doc/install/google_cloud_platform/img/change_admin_passwd_email.png delete mode 100644 doc/install/google_cloud_platform/img/chrome_not_secure_page.png create mode 100644 doc/install/google_cloud_platform/img/first_signin.png delete mode 100644 doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png delete mode 100644 doc/install/google_cloud_platform/img/gcp_gitlab_overview.png delete mode 100644 doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png delete mode 100644 doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png delete mode 100644 doc/install/google_cloud_platform/img/gitlab_deployed_page.png delete mode 100644 doc/install/google_cloud_platform/img/gitlab_first_sign_in.png delete mode 100644 doc/install/google_cloud_platform/img/gitlab_launch_button.png create mode 100644 doc/install/google_cloud_platform/img/launch_vm.png delete mode 100644 doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png create mode 100644 doc/install/google_cloud_platform/img/ssh_terminal.png delete mode 100644 doc/install/google_cloud_platform/img/ssh_via_button.png create mode 100644 doc/install/google_cloud_platform/img/vm_created.png create mode 100644 doc/install/google_cloud_platform/img/vm_details.png diff --git a/doc/install/google_cloud_platform/img/boot_disk.png b/doc/install/google_cloud_platform/img/boot_disk.png new file mode 100644 index 00000000000..37b2d9eaae7 Binary files /dev/null and b/doc/install/google_cloud_platform/img/boot_disk.png differ diff --git a/doc/install/google_cloud_platform/img/change_admin_passwd_email.png b/doc/install/google_cloud_platform/img/change_admin_passwd_email.png deleted file mode 100644 index 1ffe14f60ff..00000000000 Binary files a/doc/install/google_cloud_platform/img/change_admin_passwd_email.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/chrome_not_secure_page.png b/doc/install/google_cloud_platform/img/chrome_not_secure_page.png deleted file mode 100644 index e732066908f..00000000000 Binary files a/doc/install/google_cloud_platform/img/chrome_not_secure_page.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/first_signin.png b/doc/install/google_cloud_platform/img/first_signin.png new file mode 100644 index 00000000000..6eb3392d674 Binary files /dev/null and b/doc/install/google_cloud_platform/img/first_signin.png differ diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png b/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png deleted file mode 100644 index 2a1859da6e3..00000000000 Binary files a/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png b/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png deleted file mode 100644 index 1c4c870dbc9..00000000000 Binary files a/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/gcp_landing.png b/doc/install/google_cloud_platform/img/gcp_landing.png index 6398d247ba0..d6390c4dd4f 100644 Binary files a/doc/install/google_cloud_platform/img/gcp_landing.png and b/doc/install/google_cloud_platform/img/gcp_landing.png differ diff --git a/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png b/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png deleted file mode 100644 index f492888ea4a..00000000000 Binary files a/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png b/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png deleted file mode 100644 index b38af3966e2..00000000000 Binary files a/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/gitlab_deployed_page.png b/doc/install/google_cloud_platform/img/gitlab_deployed_page.png deleted file mode 100644 index fef9ae45f32..00000000000 Binary files a/doc/install/google_cloud_platform/img/gitlab_deployed_page.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png b/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png deleted file mode 100644 index 381c0fe48a5..00000000000 Binary files a/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/gitlab_launch_button.png b/doc/install/google_cloud_platform/img/gitlab_launch_button.png deleted file mode 100644 index 50f66f66118..00000000000 Binary files a/doc/install/google_cloud_platform/img/gitlab_launch_button.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/launch_vm.png b/doc/install/google_cloud_platform/img/launch_vm.png new file mode 100644 index 00000000000..3fd13f232bb Binary files /dev/null and b/doc/install/google_cloud_platform/img/launch_vm.png differ diff --git a/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png b/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png deleted file mode 100644 index 00060841619..00000000000 Binary files a/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/ssh_terminal.png b/doc/install/google_cloud_platform/img/ssh_terminal.png new file mode 100644 index 00000000000..6a1a418d8e9 Binary files /dev/null and b/doc/install/google_cloud_platform/img/ssh_terminal.png differ diff --git a/doc/install/google_cloud_platform/img/ssh_via_button.png b/doc/install/google_cloud_platform/img/ssh_via_button.png deleted file mode 100644 index 26106f159ad..00000000000 Binary files a/doc/install/google_cloud_platform/img/ssh_via_button.png and /dev/null differ diff --git a/doc/install/google_cloud_platform/img/vm_created.png b/doc/install/google_cloud_platform/img/vm_created.png new file mode 100644 index 00000000000..fb467f40838 Binary files /dev/null and b/doc/install/google_cloud_platform/img/vm_created.png differ diff --git a/doc/install/google_cloud_platform/img/vm_details.png b/doc/install/google_cloud_platform/img/vm_details.png new file mode 100644 index 00000000000..2d230416a4b Binary files /dev/null and b/doc/install/google_cloud_platform/img/vm_details.png differ diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md index c6b767fff02..3389f0260f9 100644 --- a/doc/install/google_cloud_platform/index.md +++ b/doc/install/google_cloud_platform/index.md @@ -2,12 +2,7 @@ ![GCP landing page](img/gcp_landing.png) -The fastest way to get started on [Google Cloud Platform (GCP)][gcp] is through -the [Google Cloud Launcher][launcher] program. - -GitLab's official Google Launcher apps: -1. [GitLab Community Edition](https://console.cloud.google.com/launcher/details/gitlab-public/gitlab-community-edition?project=gitlab-public) -2. [GitLab Enterprise Edition](https://console.cloud.google.com/launcher/details/gitlab-public/gitlab-enterprise-edition?project=gitlab-public) +Gettung started with GitLab on a [Google Cloud Platform (GCP)][gcp] instance is quick and easy. ## Prerequisites @@ -17,84 +12,52 @@ There are only two prerequisites in order to install GitLab on GCP: 1. You need to sign up for the GCP program. If this is your first time, Google gives you [$300 credit for free][freetrial] to consume over a 60-day period. -Once you have performed those two steps, you can visit the -[GCP launcher console][console] which has a list of all the things you can -deploy on GCP. - -![GCP launcher console](img/gcp_launcher_console_home_page.png) - -The next step is to find and install GitLab. +Once you have performed those two steps, you can [create a VM](#creating-the-vm). -## Configuring and deploying the VM +## Creating the VM To deploy GitLab on GCP you need to follow five simple steps: -1. Go to https://cloud.google.com/launcher and login with your Google credentials -1. Search for GitLab from GitLab Inc. (not the same as Bitnami) and click on - the tile. +1. Go to https://console.cloud.google.com/compute/instances and login with your Google credentials. - ![Search for GitLab](img/gcp_search_for_gitlab.png) +1. Click on **Create** -1. In the next page, you can see an overview of the GitLab VM as well as some - estimated costs. Click the **Launch on Compute Engine** button to choose the - hardware and network settings. + ![Search for GitLab](img/launch_vm.png) - ![Launch on Compute Engine](img/gcp_gitlab_overview.png) +1. On the next page, you can select the type of VM as well as the + estimated costs. Provide the name of the instance, desired datacenter, and machine type. Note that GitLab recommends at least 2 vCPU's and 4GB of RAM. -1. In the settings page you can choose things like the datacenter where your GitLab - server will be hosted, the number of CPUs and amount of RAM, the disk size - and type, etc. Read GitLab's [requirements documentation][req] for more - details on what to choose depending on your needs. + ![Launch on Compute Engine](img/vm_details.png) - ![Deploy settings](img/new_gitlab_deployment_settings.png) +1. Click **Change** under Boot disk to select the size, type, and desired operating system. GitLab supports a [variety of linux operating systems][req], including Ubuntu and Debian. Click **Select** when finished. -1. As a last step, hit **Deploy** when ready. The process will finish in a few - seconds. + ![Deploy in progress](img/boot_disk.png) - ![Deploy in progress](img/gcp_gitlab_being_deployed.png) +1. As a last step allow HTTP and HTTPS traffic, then click **Create**. The process will finish in a few seconds. +## Installing GitLab -## Visiting GitLab for the first time +After a few seconds, the instance will be created and available to log in. The next step is to install GitLab onto the instance. -After a few seconds, GitLab will be successfully deployed and you should be -able to see the IP address that Google assigned to the VM, as well as the -credentials to the GitLab admin account. +![Deploy settings](img/vm_created.png) -![Deploy settings](img/gitlab_deployed_page.png) +1. Make a note of the IP address of the instance, as you will need that in a later step. +1. Click on the SSH button to connect to the instance. +1. A new window will appear, with you logged into the instance. -1. Click on the IP under **Site address** to visit GitLab. -1. Accept the self-signed certificate that Google automatically deployed in - order to securely reach GitLab's login page. -1. Use the username and password that are present in the Google console page - to login into GitLab and click **Sign in**. + ![GitLab first sign in](img/ssh_terminal.png) - ![GitLab first sign in](img/gitlab_first_sign_in.png) +1. Next, follow the instructions for installing GitLab for the operating system you choose, at https://about.gitlab.com/installation/. You can use the IP address from the step above, as the hostname. -Congratulations! GitLab is now installed and you can access it via your browser, -but we're not done yet. There are some steps you need to take in order to have -a fully functional GitLab installation. +1. Congratulations! GitLab is now installed and you can access it via your browser. To finish installation, open the URL in your browser and provide the initial administrator password. The username for this account is `root`. + + ![GitLab first sign in](img/first_signin.png) ## Next steps These are the most important next steps to take after you installed GitLab for the first time. -### Changing the admin password and email - -Google assigned a random password for the GitLab admin account and you should -change it ASAP: - -1. Visit the GitLab admin page through the link in the Google console under - **Admin URL**. -1. Find the Administrator user under the **Users** page and hit **Edit**. -1. Change the email address to a real one and enter a new password. - - ![Change GitLab admin password](img/change_admin_passwd_email.png) - -1. Hit **Save changes** for the changes to take effect. -1. After changing the password, you will be signed out from GitLab. Use the - new credentials to login again. - ### Assigning a static IP By default, Google assigns an ephemeral IP to your instance. It is strongly @@ -112,7 +75,7 @@ here's how you configure GitLab to be aware of the change: 1. SSH into the VM. You can easily use the **SSH** button in the Google console and a new window will pop up. - ![SSH button](img/ssh_via_button.png) + ![SSH button](img/vm_created.png) In the future you might want to set up [connecting with an SSH key][ssh] instead. @@ -161,7 +124,6 @@ Kerberos, etc. Here are some documents you might be interested in reading: - [GitLab Pages configuration](https://docs.gitlab.com/ce/administration/pages/index.html) - [GitLab Container Registry configuration](https://docs.gitlab.com/ce/administration/container_registry.html) -[console]: https://console.cloud.google.com/launcher "GCP launcher console" [freetrial]: https://console.cloud.google.com/freetrial "GCP free trial" [ip]: https://cloud.google.com/compute/docs/configure-instance-ip-addresses#promote_ephemeral_ip "Configuring an Instance's IP Addresses" [gcp]: https://cloud.google.com/ "Google Cloud Platform" -- cgit v1.2.1 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: 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 -- cgit v1.2.1 From e887e858716738c937473776f94d0686c628b2c6 Mon Sep 17 00:00:00 2001 From: Shah El-Rahman Date: Tue, 13 Feb 2018 22:16:36 -0600 Subject: Remove use of callback in pipeline stop/retry modals Make changes to names of events and variables Combine stop and retry modals into 1 Fix lint issues Add comment about usage of eventHub instead of props --- .../pipelines/components/async_button.vue | 30 ++++++--- .../pipelines/components/pipelines_table.vue | 71 ++++++++++++++++++++-- .../pipelines/components/pipelines_table_row.vue | 10 +-- .../components/retry_confirmation_modal.vue | 65 -------------------- .../components/stop_confirmation_modal.vue | 65 -------------------- spec/javascripts/pipelines/async_button_spec.js | 11 ++-- 6 files changed, 100 insertions(+), 152 deletions(-) delete mode 100644 app/assets/javascripts/pipelines/components/retry_confirmation_modal.vue delete mode 100644 app/assets/javascripts/pipelines/components/stop_confirmation_modal.vue diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index a5f22c4ec80..0cdffbde05b 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -31,10 +31,14 @@ type: String, required: true, }, - id: { + pipelineId: { type: Number, required: true, }, + type: { + type: String, + required: true, + }, }, data() { return { @@ -46,17 +50,27 @@ return `btn ${this.cssClass}`; }, }, + created() { + // We're using eventHub to listen to the modal here instead of + // using props because it would would make the parent components + // much more complex to keep track of the loading state of each button + eventHub.$on('postAction', this.setLoading); + }, + beforeDestroy() { + eventHub.$off('postAction', this.setLoading); + }, methods: { onClick() { - eventHub.$emit('actionConfirmationModal', { - id: this.id, - callback: this.makeRequest, + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipelineId, + endpoint: this.endpoint, + type: this.type, }); }, - makeRequest() { - this.isLoading = true; - - eventHub.$emit('postAction', this.endpoint); + setLoading(endpoint) { + if (endpoint === this.endpoint) { + this.isLoading = true; + } }, }, }; diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 62fe479fdf4..c9028952ddd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -1,7 +1,8 @@ diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 0e3a10ed7f4..33d441e573e 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -305,9 +305,10 @@ css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" icon="repeat" - :id="pipeline.id" + :pipeline-id="pipeline.id" data-toggle="modal" - data-target="#retry-confirmation-modal" + data-target="#confirmation-modal" + type="retry" /> diff --git a/app/assets/javascripts/pipelines/components/retry_confirmation_modal.vue b/app/assets/javascripts/pipelines/components/retry_confirmation_modal.vue deleted file mode 100644 index e2ac08d67bc..00000000000 --- a/app/assets/javascripts/pipelines/components/retry_confirmation_modal.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/app/assets/javascripts/pipelines/components/stop_confirmation_modal.vue b/app/assets/javascripts/pipelines/components/stop_confirmation_modal.vue deleted file mode 100644 index d737d567787..00000000000 --- a/app/assets/javascripts/pipelines/components/stop_confirmation_modal.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 8ce33d410a7..e0ea3649646 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -15,7 +15,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'repeat', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); }); @@ -39,8 +40,9 @@ describe('Pipelines Async Button', () => { describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { - eventHub.$on('actionConfirmationModal', (data) => { - expect(data.id).toEqual(123); + eventHub.$on('openConfirmationModal', (data) => { + expect(data.pipelineId).toEqual(123); + expect(data.type).toEqual('explode'); }); component = new AsyncButtonComponent({ @@ -49,7 +51,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); -- cgit v1.2.1 From d8dfec0ffa4fdbb383bf4f0d1ef71969adb50dd3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 16 Feb 2018 18:03:34 +0100 Subject: Fix project import API after import service refactor --- lib/api/project_import.rb | 8 ++++++-- spec/requests/api/project_import_spec.rb | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index c32e2f26ae3..a509c1f32c1 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -41,8 +41,12 @@ module API current_user.namespace end - project_params = import_params.merge(namespace_id: namespace.id, - file: import_params[:file]['tempfile']) + project_params = { + path: import_params[:path], + namespace_id: namespace.id, + file: import_params[:file]['tempfile'] + } + project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute render_api_error!(project.errors.full_messages&.first, 400) unless project.saved? diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index eabf9095c13..987f6e26971 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -42,7 +42,7 @@ describe API::ProjectImport do it 'schedules an import at the user namespace level' do expect_any_instance_of(Project).not_to receive(:import_schedule) - expect(Gitlab::ImportExport::ProjectCreator).not_to receive(:new) + expect(::Projects::CreateService).not_to receive(:new) post api('/projects/import', user), namespace: 'nonexistent', path: 'test-import2', file: fixture_file_upload(file) @@ -73,7 +73,7 @@ describe API::ProjectImport do def stub_import(namespace) expect_any_instance_of(Project).to receive(:import_schedule) - expect(Gitlab::ImportExport::ProjectCreator).to receive(:new).with(namespace.id, any_args).and_call_original + expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original end end -- cgit v1.2.1 From 5d58949ea6ff3fb3ccb908518a0aef1f5bf6693e Mon Sep 17 00:00:00 2001 From: Ian Baum Date: Fri, 16 Feb 2018 12:13:39 -0500 Subject: Update CHANGELOG.md for 10.4.4 [ci skip] --- CHANGELOG.md | 19 +++++++++++++++++++ ...equest-undefined-method-index-for-nil-nilclass.yml | 5 ----- changelogs/unreleased/42591-update-nokogiri.yml | 5 ----- ...-gitlab-import-leaves-group_id-on-projectlabel.yml | 5 ----- ...0-on-fork-without-restricted-visibility-levels.yml | 5 ----- .../dm-user-namespace-route-path-validation.yml | 5 ----- .../fj-37528-error-after-disabling-ldap.yml | 6 ------ .../mk-fix-no-untracked-upload-files-error.yml | 5 ----- .../unreleased/remove_ldap_person_validation.yml | 5 ----- changelogs/unreleased/sh-fix-jira-trailing-slash.yml | 5 ----- .../unreleased/winh-new-branch-dropdown-style.yml | 5 ----- 11 files changed, 19 insertions(+), 51 deletions(-) delete mode 100644 changelogs/unreleased/42160-error-500-loading-merge-request-undefined-method-index-for-nil-nilclass.yml delete mode 100644 changelogs/unreleased/42591-update-nokogiri.yml delete mode 100644 changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml delete mode 100644 changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml delete mode 100644 changelogs/unreleased/dm-user-namespace-route-path-validation.yml delete mode 100644 changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml delete mode 100644 changelogs/unreleased/mk-fix-no-untracked-upload-files-error.yml delete mode 100644 changelogs/unreleased/remove_ldap_person_validation.yml delete mode 100644 changelogs/unreleased/sh-fix-jira-trailing-slash.yml delete mode 100644 changelogs/unreleased/winh-new-branch-dropdown-style.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad603fdc75..c3bb93fbe3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.4.4 (2018-02-16) + +### Security (1 change) + +- Update nokogiri to 1.8.2. !16807 + +### Fixed (9 changes) + +- Fix 500 error when loading a merge request with an invalid comment. !16795 +- Cleanup new branch/merge request form in issues. !16854 +- Fix GitLab import leaving group_id on ProjectLabel. !16877 +- Fix forking projects when no restricted visibility levels are defined applicationwide. !16881 +- Resolve PrepareUntrackedUploads PostgreSQL syntax error. !17019 +- Fixed error 500 when removing an identity with synced attributes and visiting the profile page. !17054 +- Validate user namespace before saving so that errors persist on model. +- LDAP Person no longer throws exception on invalid entry. +- Fix JIRA not working when a trailing slash is included. + + ## 10.4.3 (2018-02-05) ### Security (4 changes) diff --git a/changelogs/unreleased/42160-error-500-loading-merge-request-undefined-method-index-for-nil-nilclass.yml b/changelogs/unreleased/42160-error-500-loading-merge-request-undefined-method-index-for-nil-nilclass.yml deleted file mode 100644 index 64340ab08cd..00000000000 --- a/changelogs/unreleased/42160-error-500-loading-merge-request-undefined-method-index-for-nil-nilclass.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix 500 error when loading a merge request with an invalid comment -merge_request: 16795 -author: -type: fixed diff --git a/changelogs/unreleased/42591-update-nokogiri.yml b/changelogs/unreleased/42591-update-nokogiri.yml deleted file mode 100644 index 5f9587d2d92..00000000000 --- a/changelogs/unreleased/42591-update-nokogiri.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update nokogiri to 1.8.2 -merge_request: 16807 -author: -type: security diff --git a/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml b/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml deleted file mode 100644 index c46cc86c99b..00000000000 --- a/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix GitLab import leaving group_id on ProjectLabel -merge_request: 16877 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml b/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml deleted file mode 100644 index 378f0ef7ce9..00000000000 --- a/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix forking projects when no restricted visibility levels are defined applicationwide -merge_request: 16881 -author: -type: fixed diff --git a/changelogs/unreleased/dm-user-namespace-route-path-validation.yml b/changelogs/unreleased/dm-user-namespace-route-path-validation.yml deleted file mode 100644 index 36615e5b976..00000000000 --- a/changelogs/unreleased/dm-user-namespace-route-path-validation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Validate user namespace before saving so that errors persist on model -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml b/changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml deleted file mode 100644 index 1e35f2b537d..00000000000 --- a/changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fixed error 500 when removing an identity with synced attributes and visiting - the profile page -merge_request: 17054 -author: -type: fixed diff --git a/changelogs/unreleased/mk-fix-no-untracked-upload-files-error.yml b/changelogs/unreleased/mk-fix-no-untracked-upload-files-error.yml deleted file mode 100644 index fddfba94192..00000000000 --- a/changelogs/unreleased/mk-fix-no-untracked-upload-files-error.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve PrepareUntrackedUploads PostgreSQL syntax error -merge_request: 17019 -author: -type: fixed diff --git a/changelogs/unreleased/remove_ldap_person_validation.yml b/changelogs/unreleased/remove_ldap_person_validation.yml deleted file mode 100644 index da7f0a52886..00000000000 --- a/changelogs/unreleased/remove_ldap_person_validation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: LDAP Person no longer throws exception on invalid entry -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-jira-trailing-slash.yml b/changelogs/unreleased/sh-fix-jira-trailing-slash.yml deleted file mode 100644 index 786f6cd3727..00000000000 --- a/changelogs/unreleased/sh-fix-jira-trailing-slash.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix JIRA not working when a trailing slash is included -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/winh-new-branch-dropdown-style.yml b/changelogs/unreleased/winh-new-branch-dropdown-style.yml deleted file mode 100644 index 007e9e9f453..00000000000 --- a/changelogs/unreleased/winh-new-branch-dropdown-style.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Cleanup new branch/merge request form in issues -merge_request: 16854 -author: -type: fixed -- cgit v1.2.1 From 5bb6124ce73379f708b10e7f4d6f98ed761ee3f6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 16 Feb 2018 17:20:28 +0000 Subject: 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; } -- cgit v1.2.1 From d54a80ca3a25612ffb85e47de769a7967999a232 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Fri, 16 Feb 2018 16:01:35 -0200 Subject: Update all articles with the new layout (meta data from the frontmatter) Context: https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/182 --- .../auth/how_to_configure_ldap_gitlab_ce/index.md | 13 +++++---- doc/ci/examples/artifactory_and_gitlab/index.md | 10 +++---- .../laravel_with_gitlab_and_envoy/index.md | 10 +++---- doc/development/writing_documentation.md | 31 +++++++++++++--------- doc/install/openshift_and_gitlab/index.md | 13 +++++---- doc/topics/git/how_to_install_git/index.md | 13 +++++---- .../numerous_undo_possibilities_in_git/index.md | 13 +++++---- .../project/pages/getting_started_part_four.md | 13 +++++---- doc/user/project/pages/getting_started_part_one.md | 13 +++++---- .../project/pages/getting_started_part_three.md | 11 ++++---- doc/user/project/pages/getting_started_part_two.md | 13 +++++---- 11 files changed, 90 insertions(+), 63 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: - -- 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** - -```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 +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) +- **Knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) +- **Author's name** and **GitLab.com handle** +- **Publication date** + +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** -- cgit v1.2.1 From d9b299aced0ec7ed983dd8e6ece86cef25d03e1a Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 15:35:01 -0600 Subject: migrate projects:milestones:index to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/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); -- cgit v1.2.1 From d163f906122d71a62e5c6bd2429a101a180863f6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:06:26 -0600 Subject: migrate projects:milestones:show to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/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(); -}; +}); -- cgit v1.2.1 From 5fa3bd178793d8084e495807b8b6cfa20974992b Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:09:34 -0600 Subject: 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); -- cgit v1.2.1 From feb86d49b016d2bd422f28b89284bebbe4bedf19 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:11:14 -0600 Subject: migrate dashboard:milestones:show to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/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 -}; +}); -- cgit v1.2.1 From 99bc2a4a2dbc3c53a030488d75136090b2c87aa1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:13:06 -0600 Subject: 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(); -}; +}); -- cgit v1.2.1 From 9bb3ff551c990db81246468314ef55a8a2a426b4 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:15:19 -0600 Subject: migrate dashboard:merge_requests to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/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(); -}; +}); -- cgit v1.2.1 From 17b8bfc8c6c4837b9f2fd825f8fcc192a6aaf962 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:15:57 -0600 Subject: 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(); -}; +}); -- cgit v1.2.1 From 8878dce8ce1a8a0550a5df9ee8d5f49be28abbd0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:17:00 -0600 Subject: 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(); -}; +}); -- cgit v1.2.1 From 96feb536e426e9f1f54fa14f50cab9f2a78754be Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:19:07 -0600 Subject: 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()); -- cgit v1.2.1 From e95d4822960c6541bb429479853b8232aa669335 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:22:26 -0600 Subject: 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 () => { }, }); } -}; +}); -- cgit v1.2.1 From c6d6623040e8be6c70e8d4166939143b21217658 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:27:21 -0600 Subject: migrate dashboard:projects:* to static bundle --- app/assets/javascripts/dispatcher.js | 46 ++++++++++------------ .../javascripts/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()); -- cgit v1.2.1 From 754dc137f70709eb3d7e6853ffa83f3017e93dbe Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:30:28 -0600 Subject: 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()); -- cgit v1.2.1 From 6e0f420ce95e94b1a2cd509577bf6953a48f9b6f Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:34:35 -0600 Subject: 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(); -} +}); -- cgit v1.2.1 From bd36fe31327ea5f075e6b1896d7a634afde61fa6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:41:04 -0600 Subject: 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()); -- cgit v1.2.1 From 92168708a9a2ec44206148586df81ddccc1b2bb6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:42:55 -0600 Subject: migrate projects:environments:metrics to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/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); -- cgit v1.2.1 From bba4126311257628bb136de6b7a1d83a262bbd8e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:46:54 -0600 Subject: 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); -}; +}); -- cgit v1.2.1 From 8c92330c5074ad2cc723a9bbd56d80d44b52afd9 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:48:17 -0600 Subject: migrate projects:branches:new to static bundle --- app/assets/javascripts/dispatcher.js | 10 ---------- app/assets/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)) +)); -- cgit v1.2.1 From be6bd83b4cb001ebe59c2b62f59faacc585e4fc7 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:50:24 -0600 Subject: migrate projects:branches:index to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/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 -}; +}); -- cgit v1.2.1 From a6af8e8e643a2779f54c0a07b1d79d86221aacdc Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:55:18 -0600 Subject: migrate projects:issues:new and projects:issues:edit to static bundle --- app/assets/javascripts/dispatcher.js | 10 ---------- app/assets/javascripts/pages/projects/issues/edit/index.js | 4 +--- app/assets/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); -- cgit v1.2.1 From dc65609e726b78098e82becdd6224fc87a37130d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:57:10 -0600 Subject: migrate projects:merge_requests:creations:new to static bundle --- app/assets/javascripts/dispatcher.js | 4 ---- .../javascripts/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, }); } -}; +}); -- cgit v1.2.1 From a9f320b5a2acd092c7b4523ff8ab7160c337db20 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 16:59:36 -0600 Subject: 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); -- cgit v1.2.1 From 71ac339fcaee5611506ea250bf501ecb49d202b0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 17:06:10 -0600 Subject: migrate projects:merge_requests:edit to static bundle --- app/assets/javascripts/dispatcher.js | 5 ----- app/assets/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); -- cgit v1.2.1 From fab0f24c67833a6640830febdd1b705395a13484 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 2 Feb 2018 18:14:24 -0600 Subject: 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) -- cgit v1.2.1 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: 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 -- cgit v1.2.1 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: add changelog --- changelogs/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 -- cgit v1.2.1 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: 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 -- cgit v1.2.1 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: 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) -- cgit v1.2.1 From dc9b2a8fb2a282aefa946246b0fbaf15562fb4a5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 13:01:07 -0600 Subject: correct for missing break statement in dispatcher.js --- app/assets/javascripts/dispatcher.js | 1 + .../javascripts/pages/projects/merge_requests/creations/diffs/index.js | 3 --- .../javascripts/pages/projects/merge_requests/creations/index.js | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js create mode 100644 app/assets/javascripts/pages/projects/merge_requests/creations/index.js 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/diffs/index.js deleted file mode 100644 index febfecebbd2..00000000000 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; - -document.addEventListener('DOMContentLoaded', initMergeRequest); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js new file mode 100644 index 00000000000..febfecebbd2 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js @@ -0,0 +1,3 @@ +import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; + +document.addEventListener('DOMContentLoaded', initMergeRequest); -- cgit v1.2.1 From b63674e2c677bdbfc40b89d06af86771a807f6d1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 13:08:05 -0600 Subject: migrate admin:users:* to static bundle --- app/assets/javascripts/dispatcher.js | 20 --- .../pages/admin/projects/index/index.js | 4 +- .../admin/users/components/delete_user_modal.vue | 174 +++++++++++++++++++++ app/assets/javascripts/pages/admin/users/index.js | 43 +++++ .../users/shared/components/delete_user_modal.vue | 174 --------------------- .../javascripts/pages/admin/users/shared/index.js | 43 ----- 6 files changed, 219 insertions(+), 239 deletions(-) create mode 100644 app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue create mode 100644 app/assets/javascripts/pages/admin/users/index.js delete mode 100644 app/assets/javascripts/pages/admin/users/shared/components/delete_user_modal.vue delete mode 100644 app/assets/javascripts/pages/admin/users/shared/index.js 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/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue new file mode 100644 index 00000000000..7b5e333011e --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -0,0 +1,174 @@ + + + diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js new file mode 100644 index 00000000000..4f5d6b55031 --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; +import csrf from '~/lib/utils/csrf'; + +import deleteUserModal from './components/delete_user_modal.vue'; + +document.addEventListener('DOMContentLoaded', () => { + Vue.use(Translate); + + const deleteUserModalEl = document.getElementById('delete-user-modal'); + + const deleteModal = new Vue({ + el: deleteUserModalEl, + data: { + deleteUserUrl: '', + blockUserUrl: '', + deleteContributions: '', + username: '', + }, + render(createElement) { + return createElement(deleteUserModal, { + props: { + deleteUserUrl: this.deleteUserUrl, + blockUserUrl: this.blockUserUrl, + deleteContributions: this.deleteContributions, + username: this.username, + csrfToken: csrf.token, + }, + }); + }, + }); + + $(document).on('shown.bs.modal', (event) => { + if (event.relatedTarget.classList.contains('delete-user-button')) { + const buttonProps = event.relatedTarget.dataset; + deleteModal.deleteUserUrl = buttonProps.deleteUserUrl; + deleteModal.blockUserUrl = buttonProps.blockUserUrl; + deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions'); + deleteModal.username = buttonProps.username; + } + }); +}); diff --git a/app/assets/javascripts/pages/admin/users/shared/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/shared/components/delete_user_modal.vue deleted file mode 100644 index 7b5e333011e..00000000000 --- a/app/assets/javascripts/pages/admin/users/shared/components/delete_user_modal.vue +++ /dev/null @@ -1,174 +0,0 @@ - - - diff --git a/app/assets/javascripts/pages/admin/users/shared/index.js b/app/assets/javascripts/pages/admin/users/shared/index.js deleted file mode 100644 index d2a0f82fa2b..00000000000 --- a/app/assets/javascripts/pages/admin/users/shared/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import Vue from 'vue'; - -import Translate from '~/vue_shared/translate'; -import csrf from '~/lib/utils/csrf'; - -import deleteUserModal from './components/delete_user_modal.vue'; - -export default () => { - Vue.use(Translate); - - const deleteUserModalEl = document.getElementById('delete-user-modal'); - - const deleteModal = new Vue({ - el: deleteUserModalEl, - data: { - deleteUserUrl: '', - blockUserUrl: '', - deleteContributions: '', - username: '', - }, - render(createElement) { - return createElement(deleteUserModal, { - props: { - deleteUserUrl: this.deleteUserUrl, - blockUserUrl: this.blockUserUrl, - deleteContributions: this.deleteContributions, - username: this.username, - csrfToken: csrf.token, - }, - }); - }, - }); - - $(document).on('shown.bs.modal', (event) => { - if (event.relatedTarget.classList.contains('delete-user-button')) { - const buttonProps = event.relatedTarget.dataset; - deleteModal.deleteUserUrl = buttonProps.deleteUserUrl; - deleteModal.blockUserUrl = buttonProps.blockUserUrl; - deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions'); - deleteModal.username = buttonProps.username; - } - }); -}; -- cgit v1.2.1 From 93de4ebd9efe49d9cbf7678d77b2a1574f5b4e91 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Feb 2018 13:59:22 -0600 Subject: 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" -- cgit v1.2.1 From e8d4708be9e050e7eb9aaa5db63a37f965146e57 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Feb 2018 14:28:54 -0600 Subject: 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); -- cgit v1.2.1 From c7c211c28bc9e2594bbfa2b8f737b35b32e34934 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Feb 2018 14:33:01 -0600 Subject: Wrap pages in DOMContentLoaded --- app/assets/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(); -}; +}); -- cgit v1.2.1 From 17d842db303759b48c8ed6235f462d98d68d1bca Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 16 Feb 2018 20:07:24 +0000 Subject: 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) } -- cgit v1.2.1 From a978a5e01de26ad8d96312cb0a07087ec2dccb21 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Feb 2018 22:19:22 +0000 Subject: Revert "Merge branch 'expired-ci-artifacts' into 'master'" This reverts merge request !16578 --- app/models/ci/build.rb | 37 +++------------------- changelogs/unreleased/expired-ci-artifacts.yml | 5 --- .../20180119160751_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 -- cgit v1.2.1 From 1a6dbaefb1948d81438f1d50f93deabcc59a43eb Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 16 Feb 2018 14:26:21 -0800 Subject: Add back database changes for Ci::Build --- .../20180119160751_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 -- cgit v1.2.1 From 348c60d9be8b0e358d766c46e3e6d343af3e187a Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 15 Feb 2018 13:20:55 -0800 Subject: Remove codebase dependencies from a BG migration Specifically, `PopulateUntrackedUploads` and its spec. --- .../populate_untracked_uploads.rb | 11 +- .../populate_untracked_uploads_dependencies.rb | 59 +++++++ .../populate_untracked_uploads_spec.rb | 194 ++++++++++----------- spec/support/track_untracked_uploads_helpers.rb | 123 ++++++++++++- 4 files changed, 275 insertions(+), 112 deletions(-) create mode 100644 lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb index ee55fabd6f0..c0aa64e27ed 100644 --- a/lib/gitlab/background_migration/populate_untracked_uploads.rb +++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb @@ -5,11 +5,15 @@ module Gitlab # This class processes a batch of rows in `untracked_files_for_uploads` by # adding each file to the `uploads` table if it does not exist. class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength + include PopulateUntrackedUploadsDependencies + # This class is responsible for producing the attributes necessary to # track an uploaded file in the `uploads` table. class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength self.table_name = 'untracked_files_for_uploads' + include PopulateUntrackedUploadsDependencies + # Ends with /:random_hex/:filename FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z} FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/ @@ -147,11 +151,6 @@ module Gitlab end end - # This class is used to query the `uploads` table. - class Upload < ActiveRecord::Base - self.table_name = 'uploads' - end - def perform(start_id, end_id) return unless migrate? @@ -229,7 +228,7 @@ module Gitlab end ids.each do |model_type, model_ids| - model_class = Object.const_get(model_type) + model_class = self.class.const_get(model_type) found_ids = model_class.where(id: model_ids.uniq).pluck(:id) deleted_ids = ids[model_type] - found_ids ids[model_type] = deleted_ids diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb new file mode 100644 index 00000000000..188aeed4fbf --- /dev/null +++ b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +module Gitlab + module BackgroundMigration + module PopulateUntrackedUploadsDependencies + # Avoid using application code + class Upload < ActiveRecord::Base + self.table_name = 'uploads' + end + + # Avoid using application code + class Appearance < ActiveRecord::Base + self.table_name = 'appearances' + end + + # Avoid using application code + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + end + + # Avoid using application code + class Note < ActiveRecord::Base + self.table_name = 'notes' + end + + # Avoid using application code + class User < ActiveRecord::Base + self.table_name = 'users' + end + + # Since project Markdown upload paths don't contain the project ID, we have to find the + # project by its full_path. Due to MySQL/PostgreSQL differences, and historical reasons, + # the logic is somewhat complex, so I've mostly copied it in here. + class Project < ActiveRecord::Base + self.table_name = 'projects' + + def self.find_by_full_path(path) + binary = Gitlab::Database.mysql? ? 'BINARY' : '' + order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" + where_full_path_in(path).reorder(order_sql).take + end + + def self.where_full_path_in(path) + cast_lower = Gitlab::Database.postgresql? + + path = connection.quote(path) + + where = + if cast_lower + "(LOWER(routes.path) = LOWER(#{path}))" + else + "(routes.path = #{path})" + end + + joins("INNER JOIN routes ON routes.source_id = projects.id AND routes.source_type = 'Project'").where(where) + end + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index c8fa252439a..23c602a3297 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -1,17 +1,19 @@ require 'spec_helper' -# This migration is using UploadService, which sets uploads.secret that is only -# added to the DB schema in 20180129193323. Since the test isn't isolated, we -# just use the latest schema when testing this migration. -# Ideally, the test should not use factories nor UploadService, and rely on the -# `table` helper instead. -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do include TrackUntrackedUploadsHelpers subject { described_class.new } - let!(:untracked_files_for_uploads) { described_class::UntrackedFile } - let!(:uploads) { described_class::Upload } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:notes) { table(:notes) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } before do ensure_temporary_tracking_table_exists @@ -19,30 +21,30 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end context 'with untracked files and tracked files in untracked_files_for_uploads' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user1) { create(:user, :with_avatar) } - let!(:user2) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :legacy_storage, :with_avatar) } - let!(:project2) { create(:project, :legacy_storage, :with_avatar) } + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user1) { create_user(avatar: true) } + let!(:user2) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let!(:project2) { create_project(avatar: true) } before do - UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload - UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload + add_markdown_attachment(project1) + add_markdown_attachment(project2) # File records created by PrepareUntrackedUploads - untracked_files_for_uploads.create!(path: appearance.uploads.first.path) - untracked_files_for_uploads.create!(path: appearance.uploads.last.path) - untracked_files_for_uploads.create!(path: user1.uploads.first.path) - untracked_files_for_uploads.create!(path: user2.uploads.first.path) - untracked_files_for_uploads.create!(path: project1.uploads.first.path) - untracked_files_for_uploads.create!(path: project2.uploads.first.path) - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}") - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}") + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').first.path) + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').last.path) + untracked_files_for_uploads.create!(path: get_uploads(user1, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path) + untracked_files_for_uploads.create!(path: "#{project_uploads_dir(project1).sub("#{TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") + untracked_files_for_uploads.create!(path: "#{project_uploads_dir(project2).sub("#{TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") # Untrack 4 files - user2.uploads.delete_all - project2.uploads.delete_all # 2 files: avatar and a Markdown upload - appearance.uploads.where("path like '%header_logo%'").delete_all + get_uploads(user2, 'User').delete_all + get_uploads(project2, 'Project').delete_all # 2 files: avatar and a Markdown upload + get_uploads(appearance, 'Appearance').where("path like '%header_logo%'").delete_all end it 'adds untracked files to the uploads table' do @@ -50,9 +52,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id) end.to change { uploads.count }.from(4).to(8) - expect(user2.uploads.count).to eq(1) - expect(project2.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(project2, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'deletes rows after processing them' do @@ -66,9 +68,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra it 'does not create duplicate uploads of already tracked files' do subject.perform(1, untracked_files_for_uploads.last.id) - expect(user1.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'uses the start and end batch ids [only 1st half]' do @@ -80,11 +82,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(1) - expect(appearance.uploads.count).to eq(2) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(0) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(0) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -99,11 +101,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(0) - expect(appearance.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(0) + expect(get_uploads(appearance, 'Appearance').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(2) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -122,7 +124,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'does not block a whole batch because of one bad path' do - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a") expect(untracked_files_for_uploads.count).to eq(9) expect(uploads.count).to eq(4) @@ -133,7 +135,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'an unparseable path is shown in error output' do - bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a" + bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a" untracked_files_for_uploads.create!(path: bad_path) expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/) @@ -152,103 +154,113 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra describe 'upload outcomes for each path pattern' do shared_examples_for 'non_markdown_file' do - let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:expected_upload_attrs) { model_uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - model.uploads.delete_all + model_uploads.delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) + end.to change { model_uploads.count }.from(0).to(1) - expect(model.uploads.first.attributes).to include(expected_upload_attrs) + expect(model_uploads.first.attributes).to include(expected_upload_attrs) end end context 'for an appearance logo file path' do - let(:model) { create_or_update_appearance(logo: uploaded_file) } + let(:model) { create_or_update_appearance(logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for an appearance header_logo file path' do - let(:model) { create_or_update_appearance(header_logo: uploaded_file) } + let(:model) { create_or_update_appearance(header_logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for a pre-Markdown Note attachment file path' do - let(:model) { create(:note, :with_attachment) } - let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let(:model) { create_note(attachment: true) } + let!(:expected_upload_attrs) { get_uploads(model, 'Note').first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - Upload.where(model_type: 'Note', model_id: model.id).delete_all + get_uploads(model, 'Note').delete_all end # Can't use the shared example because Note doesn't have an `uploads` association it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1) + end.to change { get_uploads(model, 'Note').count }.from(0).to(1) - expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs) + expect(get_uploads(model, 'Note').first.attributes).to include(expected_upload_attrs) end end context 'for a user avatar file path' do - let(:model) { create(:user, :with_avatar) } + let(:model) { create_user(avatar: true) } + let(:model_uploads) { get_uploads(model, 'User') } it_behaves_like 'non_markdown_file' end context 'for a group avatar file path' do - let(:model) { create(:group, :with_avatar) } + let(:model) { create_group(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Namespace') } it_behaves_like 'non_markdown_file' end context 'for a project avatar file path' do - let(:model) { create(:project, :legacy_storage, :with_avatar) } + let(:model) { create_project(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Project') } it_behaves_like 'non_markdown_file' end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:model) { create(:project, :legacy_storage) } + let(:model) { create_project } before do # Upload the file - UploadService.new(model, uploaded_file, FileUploader).execute + add_markdown_attachment(model) # Create the untracked_files_for_uploads record - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(model)}/#{get_uploads(model, 'Project').first.path}") # Save the expected upload attributes - @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') + @expected_upload_attrs = get_uploads(model, 'Project').first.attributes.slice('path', 'uploader', 'size', 'checksum') # Untrack the file - model.reload.uploads.delete_all + get_uploads(model, 'Project').delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) + end.to change { get_uploads(model, 'Project').count }.from(0).to(1) - expect(model.uploads.first.attributes).to include(@expected_upload_attrs) + expect(get_uploads(model, 'Project').first.attributes).to include(@expected_upload_attrs) end end end end -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile, :migration, schema: 20180208183958 do include TrackUntrackedUploadsHelpers - let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } before(:all) do ensure_temporary_tracking_table_exists @@ -299,10 +311,10 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns the file path relative to the project directory in uploads' do - project = create(:project, :legacy_storage) + project = create_project random_hex = SecureRandom.hex - assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") + assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") end end end @@ -352,9 +364,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns FileUploader as a string' do - project = create(:project, :legacy_storage) + project = create_project - assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') + assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') end end end @@ -404,9 +416,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns Project as a string' do - project = create(:project, :legacy_storage) + project = create_project - assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project') + assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project') end end end @@ -456,54 +468,42 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns the ID as a string' do - project = create(:project, :legacy_storage) + project = create_project - assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id) + assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id) end end end describe '#file_size' do context 'for an appearance logo file path' do - let(:appearance) { create_or_update_appearance(logo: uploaded_file) } - let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) } + let(:appearance) { create_or_update_appearance(logo: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) } it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(appearance.logo.size) + expect(untracked_file.file_size).to eq(1062) end end context 'for a project avatar file path' do - let(:project) { create(:project, :legacy_storage, avatar: uploaded_file) } - let(:untracked_file) { described_class.create!(path: project.uploads.first.path) } + let(:project) { create_project(avatar: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) } it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.avatar.size) + expect(untracked_file.file_size).to eq(1062) end end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:project) { create(:project, :legacy_storage) } - let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") } + let(:project) { create_project } + let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") } before do - UploadService.new(project, uploaded_file, FileUploader).execute + add_markdown_attachment(project) end it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.uploads.first.size) + expect(untracked_file.file_size).to eq(1062) end end end diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb index a8b3ed1f41c..16aa30f9871 100644 --- a/spec/support/track_untracked_uploads_helpers.rb +++ b/spec/support/track_untracked_uploads_helpers.rb @@ -1,16 +1,121 @@ module TrackUntrackedUploadsHelpers - def uploaded_file - fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg') - fixture_file_upload(fixture_path) + PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public') + UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads') + SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system') + UPLOAD_FILENAME = 'image.png'.freeze + FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png') + FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze + + def create_or_update_appearance(logo: false, header_logo: false) + appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo)) + + add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo + add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo + + appearance end - def ensure_temporary_tracking_table_exists - Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) + def create_group(avatar: false) + index = unique_index(:group) + group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar)) + + add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar + + group + end + + def create_note(attachment: false) + note = notes.create(attachment: (UPLOAD_FILENAME if attachment)) + + add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment + + note + end + + def create_project(avatar: false) + group = create_group + project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar)) + routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works + + add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar + + project + end + + def create_user(avatar: false) + user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100) + + add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar + + user + end + + def unique_index(name = :unnamed) + @unique_index ||= {} + @unique_index[name] ||= 0 + @unique_index[name] += 1 end - def create_or_update_appearance(attrs) - a = Appearance.first_or_initialize(title: 'foo', description: 'bar') - a.update!(attrs) - a + def add_upload(model, model_type, attachment_type, uploader) + file_path = upload_file_path(model, model_type, attachment_type) + path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '') + create_file(file_path) + + uploads.create!( + size: 1062, + path: path_relative_to_public, + model_id: model.id, + model_type: model_type == 'Group' ? 'Namespace' : model_type, + uploader: uploader, + checksum: FIXTURE_CHECKSUM + ) + end + + def add_markdown_attachment(project) + project_dir = project_uploads_dir(project) + attachment_dir = File.join(project_dir, SecureRandom.hex) + attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) + project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') + create_file(attachment_file_path) + + uploads.create!( + size: 1062, + path: project_attachment_path_relative_to_project, + model_id: project.id, + model_type: 'Project', + uploader: 'FileUploader', + checksum: FIXTURE_CHECKSUM + ) + end + + def project_uploads_dir(project) + File.join(UPLOADS_DIR, project.full_path) + end + + def upload_file_path(model, model_type, attachment_type) + dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s) + File.join(dir, UPLOAD_FILENAME) + end + + def upload_dir(model_type, attachment_type) + File.join(SYSTEM_DIR, model_type, attachment_type) + end + + def create_file(path) + File.delete(path) if File.exist?(path) + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.cp(FIXTURE_FILE_PATH, path) + end + + def get_uploads(model, model_type) + uploads.where(model_type: model_type, model_id: model.id) + end + + def get_full_path(project) + routes.find_by(source_id: project.id, source_type: 'Project').path + end + + def ensure_temporary_tracking_table_exists + Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) end end -- cgit v1.2.1 From 293c51e39900efc8ea2e5ac77af680419f89862d Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 15 Feb 2018 14:22:38 -0800 Subject: Remove more BG migration spec dependencies For the `PrepareUntrackedUploads` spec. --- .../populate_untracked_uploads_spec.rb | 4 ++-- .../prepare_untracked_uploads_spec.rb | 26 +++++++++++++--------- spec/support/track_untracked_uploads_helpers.rb | 13 +++++++---- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index 23c602a3297..a098add401a 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -38,8 +38,8 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path) untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path) untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path) - untracked_files_for_uploads.create!(path: "#{project_uploads_dir(project1).sub("#{TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") - untracked_files_for_uploads.create!(path: "#{project_uploads_dir(project2).sub("#{TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project1).sub("#{TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project2).sub("#{TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") # Untrack 4 files get_uploads(user2, 'User').delete_all diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index ca77e64ae40..e66b163f2ff 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do include TrackUntrackedUploadsHelpers - include MigrationsHelpers let!(:untracked_files_for_uploads) { described_class::UntrackedFile } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } around do |example| # Especially important so the follow-up migration does not get run @@ -15,19 +21,17 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat shared_examples 'prepares the untracked_files_for_uploads table' do context 'when files were uploaded before and after hashed storage was enabled' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :with_avatar, :legacy_storage) } - let(:project2) { create(:project) } # instantiate after enabling hashed_storage + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let(:project2) { create_project } # instantiate after enabling hashed_storage before do # Markdown upload before enabling hashed_storage - UploadService.new(project1, uploaded_file, FileUploader).execute - - stub_application_setting(hashed_storage_enabled: true) + add_markdown_attachment(project1) # Markdown upload after enabling hashed_storage - UploadService.new(project2, uploaded_file, FileUploader).execute + add_markdown_attachment(project2, hashed_storage: true) end it 'has a path field long enough for really long paths' do @@ -61,7 +65,7 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat it 'does not add hashed files to the untracked_files_for_uploads table' do described_class.new.perform - hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path + hashed_file_path = get_uploads(project2, 'Project').where(uploader: 'FileUploader').first.path expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey end diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb index 16aa30f9871..87a86dd21af 100644 --- a/spec/support/track_untracked_uploads_helpers.rb +++ b/spec/support/track_untracked_uploads_helpers.rb @@ -71,8 +71,8 @@ module TrackUntrackedUploadsHelpers ) end - def add_markdown_attachment(project) - project_dir = project_uploads_dir(project) + def add_markdown_attachment(project, hashed_storage: false) + project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project) attachment_dir = File.join(project_dir, SecureRandom.hex) attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') @@ -88,8 +88,13 @@ module TrackUntrackedUploadsHelpers ) end - def project_uploads_dir(project) - File.join(UPLOADS_DIR, project.full_path) + def legacy_project_uploads_dir(project) + namespace = namespaces.find_by(id: project.namespace_id) + File.join(UPLOADS_DIR, namespace.path, project.path) + end + + def hashed_project_uploads_dir(project) + File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa') end def upload_file_path(model, model_type, attachment_type) -- cgit v1.2.1 From 3219afff5741511b5ab544fd141207e13028f93a Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 16 Feb 2018 11:58:59 -0800 Subject: Make :migration specs a little more robust E.g. https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/52873034 --- spec/spec_helper.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 85de0a14631..5600c9c6ad5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -154,6 +154,22 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end + # The :each scope runs "inside" the example, so this hook ensures the DB is in the + # correct state before any examples' before hooks are called. This prevents a + # problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends + # on background migrations being run inline during test setup) can be broken by + # altering Sidekiq behavior in an unrelated spec like so: + # + # around do |example| + # Sidekiq::Testing.fake! do + # example.run + # end + # end + config.before(:context, :migration) do + schema_migrate_down! + end + + # Each example may call `migrate!`, so we must ensure we are migrated down every time config.before(:each, :migration) do schema_migrate_down! end -- cgit v1.2.1 From 8b727fea81b19f12366894c4a4b981250c549833 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 16 Feb 2018 15:15:32 +0100 Subject: 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 -- cgit v1.2.1 From 50cdf41e7e1bac48cb9d3e5414682e1bd59e5db9 Mon Sep 17 00:00:00 2001 From: Vicky Chijwani Date: Sat, 17 Feb 2018 22:17:21 +0530 Subject: 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}" -- cgit v1.2.1 From 3a9a80315537c48a2fb3c338e0f8c76b3f9aa06d Mon Sep 17 00:00:00 2001 From: Vicky Chijwani Date: Sat, 17 Feb 2018 23:18:31 +0530 Subject: Add changelog entry --- changelogs/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 -- cgit v1.2.1 From 46e6a9f8a0f2dc0ae4e3152646f319a7cb5abcb2 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 17 Feb 2018 21:29:22 -0800 Subject: 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 -- cgit v1.2.1 From 790ab5909b37a1eb64d7a86c21c6096441512290 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Wed, 14 Feb 2018 14:08:30 +0100 Subject: 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) -- cgit v1.2.1 From 13074cab02fb20dd16eefd7c0539bfd6a341aa02 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sun, 18 Feb 2018 13:48:28 -0600 Subject: 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 -- cgit v1.2.1 From 8d32dfef9b7eefe3d48dc63398315ba41879329c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 18 Feb 2018 21:45:51 -0800 Subject: 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 --- changelogs/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 -- cgit v1.2.1 From 15ca2d5fb5961399d8031fa3c9da818ffb9cbb0f Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 19 Feb 2018 00:02:13 -0600 Subject: Fix get a single pages domain when project path contains a period --- ...-domain-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) -- cgit v1.2.1 From fdad576838c16d0ae7d181e85a5889d8ae4e5014 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 19 Feb 2018 00:21:47 -0800 Subject: Fix Error 500 when viewing a commit with a GPG signature in Geo Closes gitlab-org/gitlab-ee#4825 --- .../unreleased/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 -- cgit v1.2.1 From d4dfa342c1f5a916d325e198ff19d3f702bfb3d9 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Thu, 15 Feb 2018 05:21:17 +0000 Subject: 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 -- cgit v1.2.1 From 0c325ce8f5bb934033448ccee0ac3e4105d68385 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 19 Feb 2018 10:09:49 +0000 Subject: 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 -- cgit v1.2.1 From 23dd313d76f2254a7a5c58283cb76236a160647b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 19 Feb 2018 11:21:23 +0000 Subject: 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 -- cgit v1.2.1 From 835e0227d8b04dd60a03aa7d089f3f4401f819f7 Mon Sep 17 00:00:00 2001 From: Norike Abe Date: Mon, 19 Feb 2018 11:21:48 +0000 Subject: 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 -- cgit v1.2.1 From ef99a3c8bc09e9f03ee9aec5c07bfeca6b7d7adf Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 19 Feb 2018 14:27:39 +0100 Subject: 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 -- cgit v1.2.1 From 6be8cd2293348ac70e6ed6bfc047823675b10d98 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Mon, 19 Feb 2018 14:22:00 +0000 Subject: 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: -- cgit v1.2.1 From 01f5035bfe2c2fa6cc71564e8a02fbcb325e3370 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 19 Feb 2018 15:11:10 +0000 Subject: Fix squash with renamed files We need to ignore the names for renamed files when configuring with sparse checkout. --- .../unreleased/fix-squash-with-renamed-files.yml | 5 ++++ lib/gitlab/git/repository.rb | 2 +- spec/lib/gitlab/git/repository_spec.rb | 31 +++++++++++++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/fix-squash-with-renamed-files.yml diff --git a/changelogs/unreleased/fix-squash-with-renamed-files.yml b/changelogs/unreleased/fix-squash-with-renamed-files.yml new file mode 100644 index 00000000000..f7cd3a84367 --- /dev/null +++ b/changelogs/unreleased/fix-squash-with-renamed-files.yml @@ -0,0 +1,5 @@ +--- +title: Fix squashing when a file is renamed +merge_request: +author: +type: fixed diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index a10bc0dd32b..4261f028d35 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -2188,7 +2188,7 @@ module Gitlab ) diff_range = "#{start_sha}...#{end_sha}" diff_files = run_git!( - %W(diff --name-only --diff-filter=a --binary #{diff_range}) + %W(diff --name-only --diff-filter=ar --binary #{diff_range}) ).chomp with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 0e9150964fa..0482c48bde8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -2204,7 +2204,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'sparse checkout', :skip_gitaly_mock do let(:expected_files) { %w(files files/js files/js/application.js) } - before do + it 'checks out only the files in the diff' do allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| m.call(*args) do worktree_path = args[0] @@ -2216,11 +2216,34 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(Dir[files_pattern]).to eq(expected) end end - end - it 'checkouts only the files in the diff' do subject end + + context 'when the diff contains a rename' do + let(:repo) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged } + let(:end_sha) { new_commit_move_file(repo).oid } + + after do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo.references.update('refs/heads/master', SeedRepo::LastCommit::ID) + end + + it 'does not include the renamed file in the sparse checkout' do + allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| + m.call(*args) do + worktree_path = args[0] + files_pattern = File.join(worktree_path, '**', '*') + + expect(Dir[files_pattern]).not_to include('CHANGELOG') + expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG') + end + end + + subject + end + end end context 'with an ASCII-8BIT diff', :skip_gitaly_mock do @@ -2230,7 +2253,7 @@ describe Gitlab::Git::Repository, seed_helper: true 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) + expect(subject).to match(/\h{40}/) end end end -- cgit v1.2.1 From cca0f0a5dae0cf561f4491c71483aec08151b9dc Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Feb 2018 21:59:05 +0530 Subject: 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; -- cgit v1.2.1 From af49e61e0d501f10299f567fec01d9da2dcebf3a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 19 Feb 2018 22:04:06 +0530 Subject: Add changelog entry --- changelogs/unreleased/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 -- cgit v1.2.1 From e345406cbc30e729d08be2fee06db331e69438b6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 19 Feb 2018 11:00:05 -0600 Subject: 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': -- cgit v1.2.1 From c2440a9b8c7a4eec9dffeaae4b10d50cff907e6a Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 20 Feb 2018 04:10:26 +1030 Subject: 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 -- cgit v1.2.1 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: Resolve "group request membership mail with too long list of "To:"" --- app/mailers/emails/members.rb | 11 +-- app/services/notification_service.rb | 17 ++++- .../42274-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 -- cgit v1.2.1 From 69b750aee9f51f1206495531e14c1f9b6d421600 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 19 Feb 2018 17:56:33 +0000 Subject: [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 + .../lib/gitlab/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 -- cgit v1.2.1 From f23a57b0c5a45ab16463199ebf2dac2da3f0287f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 19 Feb 2018 12:40:18 -0600 Subject: 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: '' -- cgit v1.2.1 From 834f455b43909525609a573df3153a413ef3c6d4 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 19 Feb 2018 20:43:10 +0000 Subject: Chart.html.haml refactor --- app/assets/javascripts/graphs/graphs_bundle.js | 4 - app/assets/javascripts/graphs/graphs_charts.js | 61 - .../pages/projects/graphs/charts/index.js | 61 + .../pages/projects/pipelines/charts/index.js | 2 +- app/views/devise/sessions/two_factor.html.haml | 2 +- app/views/profiles/_head.html.haml | 2 +- app/views/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 +- app/views/projects/blob/viewers/_pdf.html.haml | 4 +- app/views/projects/blob/viewers/_sketch.html.haml | 4 +- app/views/projects/blob/viewers/_stl.html.haml | 2 +- app/views/projects/commit/_pipelines_list.haml | 4 +- app/views/projects/commit/show.html.haml | 4 +- app/views/projects/cycle_analytics/show.html.haml | 4 +- app/views/projects/environments/folder.html.haml | 4 +- app/views/projects/environments/index.html.haml | 4 +- app/views/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 +- .../projects/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 +- .../projects/protected_branches/_index.html.haml | 2 +- app/views/projects/protected_tags/_index.html.haml | 2 +- .../projects/registry/repositories/index.html.haml | 4 +- .../projects/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 + 38 files changed, 111 insertions(+), 3593 deletions(-) delete mode 100644 app/assets/javascripts/graphs/graphs_bundle.js delete mode 100644 app/assets/javascripts/graphs/graphs_charts.js create mode 100644 app/assets/javascripts/pages/projects/graphs/charts/index.js 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/graphs/graphs_charts.js deleted file mode 100644 index ec6eab34989..00000000000 --- a/app/assets/javascripts/graphs/graphs_charts.js +++ /dev/null @@ -1,61 +0,0 @@ -import Chart from 'vendor/Chart'; -import _ from 'underscore'; - -document.addEventListener('DOMContentLoaded', () => { - const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); - - const responsiveChart = (selector, data) => { - const options = { - scaleOverlay: true, - responsive: true, - pointHitDetectionRadius: 2, - maintainAspectRatio: false, - }; - // get selector by context - const ctx = selector.get(0).getContext('2d'); - // pointing parent container to make chart.js inherit its width - const container = $(selector).parent(); - const generateChart = () => { - selector.attr('width', $(container).width()); - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8; - } - return new Chart(ctx).Bar(data, options); - }; - // enabling auto-resizing - $(window).resize(generateChart); - return generateChart(); - }; - - const chartData = data => ({ - labels: Object.keys(data), - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: _.values(data), - }], - }); - - const hourData = chartData(projectChartData.hour); - responsiveChart($('#hour-chart'), hourData); - - const dayData = chartData(projectChartData.weekDays); - responsiveChart($('#weekday-chart'), dayData); - - const monthData = chartData(projectChartData.month); - responsiveChart($('#month-chart'), monthData); - - const data = projectChartData.languages; - const ctx = $('#languages-chart').get(0).getContext('2d'); - const options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false, - }; - - new Chart(ctx).Pie(data, options); -}); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js new file mode 100644 index 00000000000..42df19c2968 --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -0,0 +1,61 @@ +import Chart from 'chart.js'; +import _ from 'underscore'; + +document.addEventListener('DOMContentLoaded', () => { + const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); + + const responsiveChart = (selector, data) => { + const options = { + scaleOverlay: true, + responsive: true, + pointHitDetectionRadius: 2, + maintainAspectRatio: false, + }; + // get selector by context + const ctx = selector.get(0).getContext('2d'); + // pointing parent container to make chart.js inherit its width + const container = $(selector).parent(); + const generateChart = () => { + selector.attr('width', $(container).width()); + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + return new Chart(ctx).Bar(data, options); + }; + // enabling auto-resizing + $(window).resize(generateChart); + return generateChart(); + }; + + const chartData = data => ({ + labels: Object.keys(data), + datasets: [{ + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: _.values(data), + }], + }); + + const hourData = chartData(projectChartData.hour); + responsiveChart($('#hour-chart'), hourData); + + const dayData = chartData(projectChartData.weekDays); + responsiveChart($('#weekday-chart'), dayData); + + const monthData = chartData(projectChartData.month); + responsiveChart($('#month-chart'), monthData); + + const data = projectChartData.languages; + const ctx = $('#languages-chart').get(0).getContext('2d'); + const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, + }; + + new Chart(ctx).Pie(data, options); +}); 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" -- cgit v1.2.1 From fb38bc3faeee0d691f8f473cf5b32d356eabb43f Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 16:41:36 -0600 Subject: remove graphs_show webpack bundle --- app/assets/javascripts/graphs/graphs_show.js | 23 -- .../javascripts/graphs/stat_graph_contributors.js | 125 --------- .../graphs/stat_graph_contributors_graph.js | 294 --------------------- .../graphs/stat_graph_contributors_util.js | 138 ---------- .../pages/projects/graphs/show/index.js | 23 ++ .../graphs/show/stat_graph_contributors.js | 125 +++++++++ .../graphs/show/stat_graph_contributors_graph.js | 294 +++++++++++++++++++++ .../graphs/show/stat_graph_contributors_util.js | 138 ++++++++++ app/views/projects/graphs/show.html.haml | 3 - config/webpack.config.js | 2 - 10 files changed, 580 insertions(+), 585 deletions(-) delete mode 100644 app/assets/javascripts/graphs/graphs_show.js delete mode 100644 app/assets/javascripts/graphs/stat_graph_contributors.js delete mode 100644 app/assets/javascripts/graphs/stat_graph_contributors_graph.js delete mode 100644 app/assets/javascripts/graphs/stat_graph_contributors_util.js create mode 100644 app/assets/javascripts/pages/projects/graphs/show/index.js create mode 100644 app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js create mode 100644 app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js create mode 100644 app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/graphs/graphs_show.js deleted file mode 100644 index b670e907a5c..00000000000 --- a/app/assets/javascripts/graphs/graphs_show.js +++ /dev/null @@ -1,23 +0,0 @@ -import flash from '../flash'; -import { __ } from '../locale'; -import axios from '../lib/utils/axios_utils'; -import ContributorsStatGraph from './stat_graph_contributors'; - -document.addEventListener('DOMContentLoaded', () => { - const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; - - axios.get(url) - .then(({ data }) => { - const graph = new ContributorsStatGraph(); - graph.init(data); - - $('#brush_change').change(() => { - graph.change_date_header(); - graph.redraw_authors(); - }); - - $('.stat-graph').fadeIn(); - $('.loading-graph').hide(); - }) - .catch(() => flash(__('Error fetching contributors data.'))); -}); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js deleted file mode 100644 index 151a4ce012c..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ /dev/null @@ -1,125 +0,0 @@ -/* 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 { 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() { - this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' }); - } - - ContributorsStatGraph.prototype.init = function(log) { - var author_commits, total_commits; - this.parsed_log = ContributorsStatGraphUtil.parse_log(log); - this.set_current_field("commits"); - total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); - this.add_master_graph(total_commits); - this.add_authors_graph(author_commits); - return this.change_date_header(); - }; - - ContributorsStatGraph.prototype.add_master_graph = function(total_data) { - this.master_graph = new ContributorsMasterGraph(total_data); - return this.master_graph.draw(); - }; - - ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { - var limited_author_data; - this.authors = []; - limited_author_data = author_data.slice(0, 100); - return _.each(limited_author_data, (function(_this) { - return function(d) { - var author_graph, author_header; - author_header = _this.create_author_header(d); - $(".contributors-list").append(author_header); - _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); - return author_graph.draw(); - }; - })(this)); - }; - - ContributorsStatGraph.prototype.format_author_commit_info = function(author) { - var commits; - commits = $('', { - "class": 'graph-author-commits-count' - }); - commits.text(n__('%d commit', '%d commits', author.commits)); - return $('').append(commits); - }; - - ContributorsStatGraph.prototype.create_author_header = function(author) { - var author_commit_info, author_commit_info_span, author_email, author_name, list_item; - list_item = $('
  • ', { - "class": 'person', - style: 'display: block;' - }); - author_name = $('

    ' + author.author_name + '

    '); - author_email = $('

    ' + author.author_email + '

    '); - author_commit_info_span = $('', { - "class": 'commits' - }); - author_commit_info = this.format_author_commit_info(author); - author_commit_info_span.html(author_commit_info); - list_item.append(author_name); - list_item.append(author_email); - list_item.append(author_commit_info_span); - return list_item; - }; - - ContributorsStatGraph.prototype.redraw_master = function() { - var total_data; - total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - this.master_graph.set_data(total_data); - return this.master_graph.redraw(); - }; - - ContributorsStatGraph.prototype.redraw_authors = function() { - var author_commits, x_domain; - $("ol").html(""); - x_domain = ContributorsGraph.prototype.x_domain; - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); - return _.each(author_commits, (function(_this) { - return function(d) { - _this.redraw_author_commit_info(d); - if (_this.authors[d.author_name] != null) { - $(_this.authors[d.author_name].list_item).appendTo("ol"); - _this.authors[d.author_name].set_data(d.dates); - return _this.authors[d.author_name].redraw(); - } - return ''; - }; - })(this)); - }; - - ContributorsStatGraph.prototype.set_current_field = function(field) { - return this.field = field; - }; - - ContributorsStatGraph.prototype.change_date_header = function() { - const x_domain = ContributorsGraph.prototype.x_domain; - const formattedDateRange = sprintf( - s__('ContributorsPage|%{startDate} – %{endDate}'), - { - startDate: this.dateFormat.format(new Date(x_domain[0])), - endDate: this.dateFormat.format(new Date(x_domain[1])), - }, - ); - return $('#date_header').text(formattedDateRange); - }; - - ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { - var author_commit_info, author_list_item, $author; - $author = this.authors[author.author_name]; - if ($author != null) { - author_list_item = $(this.authors[author.author_name].list_item); - author_commit_info = this.format_author_commit_info(author); - return author_list_item.find("span").html(author_commit_info); - } - return ''; - }; - - return ContributorsStatGraph; -})(); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js deleted file mode 100644 index 9a4012232a0..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ /dev/null @@ -1,294 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ -import _ from 'underscore'; -import { extent, max } from 'd3-array'; -import { select, event as d3Event } from 'd3-selection'; -import { scaleTime, scaleLinear } from 'd3-scale'; -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'; - -const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; - -const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; -const hasProp = {}.hasOwnProperty; - -export const ContributorsGraph = (function() { - function ContributorsGraph() {} - - ContributorsGraph.prototype.MARGIN = { - top: 20, - right: 20, - bottom: 30, - left: 50 - }; - - ContributorsGraph.prototype.x_domain = null; - - ContributorsGraph.prototype.y_domain = null; - - ContributorsGraph.prototype.dates = []; - - ContributorsGraph.set_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = data; - }; - - ContributorsGraph.set_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { - return d.date; - }); - }; - - ContributorsGraph.init_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_domain = function(data) { - ContributorsGraph.init_x_domain(data); - return ContributorsGraph.init_y_domain(data); - }; - - ContributorsGraph.set_dates = function(data) { - return ContributorsGraph.prototype.dates = data; - }; - - ContributorsGraph.prototype.set_x_domain = function() { - return this.x.domain(this.x_domain); - }; - - ContributorsGraph.prototype.set_y_domain = function() { - return this.y.domain(this.y_domain); - }; - - ContributorsGraph.prototype.set_domain = function() { - this.set_x_domain(); - return this.set_y_domain(); - }; - - ContributorsGraph.prototype.create_scale = function(width, height) { - this.x = d3.scaleTime().range([0, width]).clamp(true); - return this.y = d3.scaleLinear().range([height, 0]).nice(); - }; - - ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); - }; - - ContributorsGraph.prototype.draw_y_axis = function() { - return this.svg.append("g").attr("class", "y axis").call(this.y_axis); - }; - - ContributorsGraph.prototype.set_data = function(data) { - return this.data = data; - }; - - return ContributorsGraph; -})(); - -export const ContributorsMasterGraph = (function(superClass) { - extend(ContributorsMasterGraph, superClass); - - function ContributorsMasterGraph(data1) { - const $parentElement = $('#contributors-master'); - const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); - - this.data = data1; - this.update_content = this.update_content.bind(this); - this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right); - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.brush = null; - this.x_max_domain = null; - } - - ContributorsMasterGraph.prototype.process_dates = function(data) { - var dates; - dates = this.get_dates(data); - this.parse_dates(data); - return ContributorsGraph.set_dates(dates); - }; - - ContributorsMasterGraph.prototype.get_dates = function(data) { - return _.pluck(data, 'date'); - }; - - ContributorsMasterGraph.prototype.parse_dates = function(data) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); - return data.forEach(function(d) { - return d.date = parseDate(d.date); - }); - }; - - ContributorsMasterGraph.prototype.create_scale = function() { - return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() - .scale(this.x) - .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); - }; - - ContributorsMasterGraph.prototype.create_svg = function() { - return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsMasterGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - return x(d.date); - }).y0(this.height).y1(function(d) { - d.commits = d.commits || d.additions || d.deletions; - return y(d.commits); - }); - }; - - ContributorsMasterGraph.prototype.create_brush = function() { - return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content); - }; - - ContributorsMasterGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); - }; - - ContributorsMasterGraph.prototype.add_brush = function() { - return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); - }; - - ContributorsMasterGraph.prototype.update_content = function() { - // d3Event.selection replaces the function brush.empty() calls - if (d3Event.selection != null) { - ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert)); - } else { - ContributorsGraph.set_x_domain(this.x_max_domain); - } - return $("#brush_change").trigger('change'); - }; - - ContributorsMasterGraph.prototype.draw = function() { - this.process_dates(this.data); - this.create_scale(); - this.create_axes(); - ContributorsGraph.init_domain(this.data); - this.x_max_domain = this.x_domain; - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.create_brush(); - this.draw_path(this.data); - this.draw_x_axis(); - this.draw_y_axis(); - return this.add_brush(); - }; - - ContributorsMasterGraph.prototype.redraw = function() { - this.process_dates(this.data); - ContributorsGraph.set_y_domain(this.data); - this.set_y_domain(); - this.svg.select("path").datum(this.data); - this.svg.select("path").attr("d", this.area); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsMasterGraph; -})(ContributorsGraph); - -export const ContributorsAuthorGraph = (function(superClass) { - extend(ContributorsAuthorGraph, superClass); - - function ContributorsAuthorGraph(data1) { - this.data = data1; - // Don't split graph size in half for mobile devices. - if ($(window).width() < 768) { - this.width = $('.content').width() - 80; - } else { - this.width = ($('.content').width() / 2) - 100; - } - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.list_item = null; - } - - ContributorsAuthorGraph.prototype.create_scale = function() { - return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() - .scale(this.x) - .ticks(8) - .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); - }; - - ContributorsAuthorGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); - return x(parseDate(d)); - }).y0(this.height).y1((function(_this) { - return function(d) { - if (_this.data[d] != null) { - return y(_this.data[d]); - } else { - return y(0); - } - }; - })(this)); - }; - - ContributorsAuthorGraph.prototype.create_svg = function() { - var persons = document.querySelectorAll('.person'); - this.list_item = persons[persons.length - 1]; - return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsAuthorGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); - }; - - ContributorsAuthorGraph.prototype.draw = function() { - this.create_scale(); - this.create_axes(); - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.draw_path(this.dates); - this.draw_x_axis(); - return this.draw_y_axis(); - }; - - ContributorsAuthorGraph.prototype.redraw = function() { - this.set_domain(); - this.svg.select("path").datum(this.dates); - this.svg.select("path").attr("d", this.area); - this.svg.select(".x.axis").call(this.x_axis); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsAuthorGraph; -})(ContributorsGraph); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js deleted file mode 100644 index 77135ad1f0e..00000000000 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ /dev/null @@ -1,138 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ -import _ from 'underscore'; - -export default { - parse_log: function(log) { - var by_author, by_email, data, entry, i, len, total, normalized_email; - total = {}; - by_author = {}; - by_email = {}; - for (i = 0, len = log.length; i < len; i += 1) { - entry = log[i]; - if (total[entry.date] == null) { - this.add_date(entry.date, total); - } - normalized_email = entry.author_email.toLowerCase(); - data = by_author[entry.author_name] || by_email[normalized_email]; - if (data == null) { - data = this.add_author(entry, by_author, by_email); - } - if (!data[entry.date]) { - this.add_date(entry.date, data); - } - this.store_data(entry, total[entry.date], data[entry.date]); - } - total = _.toArray(total); - by_author = _.toArray(by_author); - return { - total: total, - by_author: by_author - }; - }, - add_date: function(date, collection) { - collection[date] = {}; - return collection[date].date = date; - }, - add_author: function(author, by_author, by_email) { - var data, normalized_email; - data = {}; - data.author_name = author.author_name; - data.author_email = author.author_email; - normalized_email = author.author_email.toLowerCase(); - by_author[author.author_name] = data; - by_email[normalized_email] = data; - return data; - }, - store_data: function(entry, total, by_author) { - this.store_commits(total, by_author); - this.store_additions(entry, total, by_author); - return this.store_deletions(entry, total, by_author); - }, - store_commits: function(total, by_author) { - this.add(total, "commits", 1); - return this.add(by_author, "commits", 1); - }, - add: function(collection, field, value) { - if (collection[field] == null) { - collection[field] = 0; - } - return collection[field] += value; - }, - store_additions: function(entry, total, by_author) { - if (entry.additions == null) { - entry.additions = 0; - } - this.add(total, "additions", entry.additions); - return this.add(by_author, "additions", entry.additions); - }, - store_deletions: function(entry, total, by_author) { - if (entry.deletions == null) { - entry.deletions = 0; - } - this.add(total, "deletions", entry.deletions); - return this.add(by_author, "deletions", entry.deletions); - }, - get_total_data: function(parsed_log, field) { - var log, total_data; - log = parsed_log.total; - total_data = this.pick_field(log, field); - return _.sortBy(total_data, function(d) { - return d.date; - }); - }, - pick_field: function(log, field) { - var total_data; - total_data = []; - _.each(log, function(d) { - return total_data.push(_.pick(d, [field, 'date'])); - }); - return total_data; - }, - get_author_data: function(parsed_log, field, date_range) { - var author_data, log; - if (date_range == null) { - date_range = null; - } - log = parsed_log.by_author; - author_data = []; - _.each(log, (function(_this) { - return function(log_entry) { - var parsed_log_entry; - parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); - if (!_.isEmpty(parsed_log_entry.dates)) { - return author_data.push(parsed_log_entry); - } - }; - })(this)); - return _.sortBy(author_data, function(d) { - return d[field]; - }).reverse(); - }, - parse_log_entry: function(log_entry, field, date_range) { - var parsed_entry; - parsed_entry = {}; - parsed_entry.author_name = log_entry.author_name; - parsed_entry.author_email = log_entry.author_email; - parsed_entry.dates = {}; - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; - _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value, key) { - if (_this.in_range(value.date, date_range)) { - parsed_entry.dates[value.date] = value[field]; - parsed_entry.commits += value.commits; - parsed_entry.additions += value.additions; - return parsed_entry.deletions += value.deletions; - } - }; - })(this)); - return parsed_entry; - }, - in_range: function(date, date_range) { - var ref; - if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { - return true; - } else { - return false; - } - } -}; diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js new file mode 100644 index 00000000000..f516ff20995 --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -0,0 +1,23 @@ +import flash from '~/flash'; +import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import ContributorsStatGraph from './stat_graph_contributors'; + +document.addEventListener('DOMContentLoaded', () => { + const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; + + axios.get(url) + .then(({ data }) => { + const graph = new ContributorsStatGraph(); + graph.init(data); + + $('#brush_change').change(() => { + graph.change_date_header(); + graph.redraw_authors(); + }); + + $('.stat-graph').fadeIn(); + $('.loading-graph').hide(); + }) + .catch(() => flash(__('Error fetching contributors data.'))); +}); diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js new file mode 100644 index 00000000000..9ac0b4c07e5 --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -0,0 +1,125 @@ +/* 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'; + +export default (function() { + function ContributorsStatGraph() { + this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' }); + } + + ContributorsStatGraph.prototype.init = function(log) { + var author_commits, total_commits; + this.parsed_log = ContributorsStatGraphUtil.parse_log(log); + this.set_current_field("commits"); + total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); + this.add_master_graph(total_commits); + this.add_authors_graph(author_commits); + return this.change_date_header(); + }; + + ContributorsStatGraph.prototype.add_master_graph = function(total_data) { + this.master_graph = new ContributorsMasterGraph(total_data); + return this.master_graph.draw(); + }; + + ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { + var limited_author_data; + this.authors = []; + limited_author_data = author_data.slice(0, 100); + return _.each(limited_author_data, (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $(".contributors-list").append(author_header); + _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + return author_graph.draw(); + }; + })(this)); + }; + + ContributorsStatGraph.prototype.format_author_commit_info = function(author) { + var commits; + commits = $('', { + "class": 'graph-author-commits-count' + }); + commits.text(n__('%d commit', '%d commits', author.commits)); + return $('').append(commits); + }; + + ContributorsStatGraph.prototype.create_author_header = function(author) { + var author_commit_info, author_commit_info_span, author_email, author_name, list_item; + list_item = $('
  • ', { + "class": 'person', + style: 'display: block;' + }); + author_name = $('

    ' + author.author_name + '

    '); + author_email = $('

    ' + author.author_email + '

    '); + author_commit_info_span = $('', { + "class": 'commits' + }); + author_commit_info = this.format_author_commit_info(author); + author_commit_info_span.html(author_commit_info); + list_item.append(author_name); + list_item.append(author_email); + list_item.append(author_commit_info_span); + return list_item; + }; + + ContributorsStatGraph.prototype.redraw_master = function() { + var total_data; + total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + this.master_graph.set_data(total_data); + return this.master_graph.redraw(); + }; + + ContributorsStatGraph.prototype.redraw_authors = function() { + var author_commits, x_domain; + $("ol").html(""); + x_domain = ContributorsGraph.prototype.x_domain; + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + return _.each(author_commits, (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + if (_this.authors[d.author_name] != null) { + $(_this.authors[d.author_name].list_item).appendTo("ol"); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + } + return ''; + }; + })(this)); + }; + + ContributorsStatGraph.prototype.set_current_field = function(field) { + return this.field = field; + }; + + ContributorsStatGraph.prototype.change_date_header = function() { + const x_domain = ContributorsGraph.prototype.x_domain; + const formattedDateRange = sprintf( + s__('ContributorsPage|%{startDate} – %{endDate}'), + { + startDate: this.dateFormat.format(new Date(x_domain[0])), + endDate: this.dateFormat.format(new Date(x_domain[1])), + }, + ); + return $('#date_header').text(formattedDateRange); + }; + + ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { + var author_commit_info, author_list_item, $author; + $author = this.authors[author.author_name]; + if ($author != null) { + author_list_item = $(this.authors[author.author_name].list_item); + author_commit_info = this.format_author_commit_info(author); + return author_list_item.find("span").html(author_commit_info); + } + return ''; + }; + + return ContributorsStatGraph; +})(); 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 new file mode 100644 index 00000000000..9a4012232a0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -0,0 +1,294 @@ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ +import _ from 'underscore'; +import { extent, max } from 'd3-array'; +import { select, event as d3Event } from 'd3-selection'; +import { scaleTime, scaleLinear } from 'd3-scale'; +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'; + +const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; + +const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; +const hasProp = {}.hasOwnProperty; + +export const ContributorsGraph = (function() { + function ContributorsGraph() {} + + ContributorsGraph.prototype.MARGIN = { + top: 20, + right: 20, + bottom: 30, + left: 50 + }; + + ContributorsGraph.prototype.x_domain = null; + + ContributorsGraph.prototype.y_domain = null; + + ContributorsGraph.prototype.dates = []; + + ContributorsGraph.set_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = data; + }; + + ContributorsGraph.set_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return d.date; + }); + }; + + ContributorsGraph.init_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_domain = function(data) { + ContributorsGraph.init_x_domain(data); + return ContributorsGraph.init_y_domain(data); + }; + + ContributorsGraph.set_dates = function(data) { + return ContributorsGraph.prototype.dates = data; + }; + + ContributorsGraph.prototype.set_x_domain = function() { + return this.x.domain(this.x_domain); + }; + + ContributorsGraph.prototype.set_y_domain = function() { + return this.y.domain(this.y_domain); + }; + + ContributorsGraph.prototype.set_domain = function() { + this.set_x_domain(); + return this.set_y_domain(); + }; + + ContributorsGraph.prototype.create_scale = function(width, height) { + this.x = d3.scaleTime().range([0, width]).clamp(true); + return this.y = d3.scaleLinear().range([height, 0]).nice(); + }; + + ContributorsGraph.prototype.draw_x_axis = function() { + return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + }; + + ContributorsGraph.prototype.draw_y_axis = function() { + return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + }; + + ContributorsGraph.prototype.set_data = function(data) { + return this.data = data; + }; + + return ContributorsGraph; +})(); + +export const ContributorsMasterGraph = (function(superClass) { + extend(ContributorsMasterGraph, superClass); + + function ContributorsMasterGraph(data1) { + const $parentElement = $('#contributors-master'); + const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + + this.data = data1; + this.update_content = this.update_content.bind(this); + this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right); + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.brush = null; + this.x_max_domain = null; + } + + ContributorsMasterGraph.prototype.process_dates = function(data) { + var dates; + dates = this.get_dates(data); + this.parse_dates(data); + return ContributorsGraph.set_dates(dates); + }; + + ContributorsMasterGraph.prototype.get_dates = function(data) { + return _.pluck(data, 'date'); + }; + + ContributorsMasterGraph.prototype.parse_dates = function(data) { + var parseDate; + parseDate = d3.timeParse("%Y-%m-%d"); + return data.forEach(function(d) { + return d.date = parseDate(d.date); + }); + }; + + ContributorsMasterGraph.prototype.create_scale = function() { + return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsMasterGraph.prototype.create_axes = function() { + this.x_axis = d3.axisBottom() + .scale(this.x) + .tickFormat(dateTickFormat); + return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + }; + + ContributorsMasterGraph.prototype.create_svg = function() { + return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsMasterGraph.prototype.create_area = function(x, y) { + return this.area = d3.area().x(function(d) { + return x(d.date); + }).y0(this.height).y1(function(d) { + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); + }); + }; + + ContributorsMasterGraph.prototype.create_brush = function() { + return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content); + }; + + ContributorsMasterGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + }; + + ContributorsMasterGraph.prototype.add_brush = function() { + return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + }; + + ContributorsMasterGraph.prototype.update_content = function() { + // d3Event.selection replaces the function brush.empty() calls + if (d3Event.selection != null) { + ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert)); + } else { + ContributorsGraph.set_x_domain(this.x_max_domain); + } + return $("#brush_change").trigger('change'); + }; + + ContributorsMasterGraph.prototype.draw = function() { + this.process_dates(this.data); + this.create_scale(); + this.create_axes(); + ContributorsGraph.init_domain(this.data); + this.x_max_domain = this.x_domain; + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.create_brush(); + this.draw_path(this.data); + this.draw_x_axis(); + this.draw_y_axis(); + return this.add_brush(); + }; + + ContributorsMasterGraph.prototype.redraw = function() { + this.process_dates(this.data); + ContributorsGraph.set_y_domain(this.data); + this.set_y_domain(); + this.svg.select("path").datum(this.data); + this.svg.select("path").attr("d", this.area); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsMasterGraph; +})(ContributorsGraph); + +export const ContributorsAuthorGraph = (function(superClass) { + extend(ContributorsAuthorGraph, superClass); + + function ContributorsAuthorGraph(data1) { + this.data = data1; + // Don't split graph size in half for mobile devices. + if ($(window).width() < 768) { + this.width = $('.content').width() - 80; + } else { + this.width = ($('.content').width() / 2) - 100; + } + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.list_item = null; + } + + ContributorsAuthorGraph.prototype.create_scale = function() { + return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsAuthorGraph.prototype.create_axes = function() { + this.x_axis = d3.axisBottom() + .scale(this.x) + .ticks(8) + .tickFormat(dateTickFormat); + return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + }; + + ContributorsAuthorGraph.prototype.create_area = function(x, y) { + return this.area = d3.area().x(function(d) { + var parseDate; + parseDate = d3.timeParse("%Y-%m-%d"); + return x(parseDate(d)); + }).y0(this.height).y1((function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this)); + }; + + ContributorsAuthorGraph.prototype.create_svg = function() { + var persons = document.querySelectorAll('.person'); + this.list_item = persons[persons.length - 1]; + return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsAuthorGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + }; + + ContributorsAuthorGraph.prototype.draw = function() { + this.create_scale(); + this.create_axes(); + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.draw_path(this.dates); + this.draw_x_axis(); + return this.draw_y_axis(); + }; + + ContributorsAuthorGraph.prototype.redraw = function() { + this.set_domain(); + this.svg.select("path").datum(this.dates); + this.svg.select("path").attr("d", this.area); + this.svg.select(".x.axis").call(this.x_axis); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsAuthorGraph; +})(ContributorsGraph); diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js new file mode 100644 index 00000000000..77135ad1f0e --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -0,0 +1,138 @@ +/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ +import _ from 'underscore'; + +export default { + parse_log: function(log) { + var by_author, by_email, data, entry, i, len, total, normalized_email; + total = {}; + by_author = {}; + by_email = {}; + for (i = 0, len = log.length; i < len; i += 1) { + entry = log[i]; + if (total[entry.date] == null) { + this.add_date(entry.date, total); + } + normalized_email = entry.author_email.toLowerCase(); + data = by_author[entry.author_name] || by_email[normalized_email]; + if (data == null) { + data = this.add_author(entry, by_author, by_email); + } + if (!data[entry.date]) { + this.add_date(entry.date, data); + } + this.store_data(entry, total[entry.date], data[entry.date]); + } + total = _.toArray(total); + by_author = _.toArray(by_author); + return { + total: total, + by_author: by_author + }; + }, + add_date: function(date, collection) { + collection[date] = {}; + return collection[date].date = date; + }, + add_author: function(author, by_author, by_email) { + var data, normalized_email; + data = {}; + data.author_name = author.author_name; + data.author_email = author.author_email; + normalized_email = author.author_email.toLowerCase(); + by_author[author.author_name] = data; + by_email[normalized_email] = data; + return data; + }, + store_data: function(entry, total, by_author) { + this.store_commits(total, by_author); + this.store_additions(entry, total, by_author); + return this.store_deletions(entry, total, by_author); + }, + store_commits: function(total, by_author) { + this.add(total, "commits", 1); + return this.add(by_author, "commits", 1); + }, + add: function(collection, field, value) { + if (collection[field] == null) { + collection[field] = 0; + } + return collection[field] += value; + }, + store_additions: function(entry, total, by_author) { + if (entry.additions == null) { + entry.additions = 0; + } + this.add(total, "additions", entry.additions); + return this.add(by_author, "additions", entry.additions); + }, + store_deletions: function(entry, total, by_author) { + if (entry.deletions == null) { + entry.deletions = 0; + } + this.add(total, "deletions", entry.deletions); + return this.add(by_author, "deletions", entry.deletions); + }, + get_total_data: function(parsed_log, field) { + var log, total_data; + log = parsed_log.total; + total_data = this.pick_field(log, field); + return _.sortBy(total_data, function(d) { + return d.date; + }); + }, + pick_field: function(log, field) { + var total_data; + total_data = []; + _.each(log, function(d) { + return total_data.push(_.pick(d, [field, 'date'])); + }); + return total_data; + }, + get_author_data: function(parsed_log, field, date_range) { + var author_data, log; + if (date_range == null) { + date_range = null; + } + log = parsed_log.by_author; + author_data = []; + _.each(log, (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this)); + return _.sortBy(author_data, function(d) { + return d[field]; + }).reverse(); + }, + parse_log_entry: function(log_entry, field, date_range) { + var parsed_entry; + parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; + parsed_entry.author_email = log_entry.author_email; + parsed_entry.dates = {}; + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return parsed_entry.deletions += value.deletions; + } + }; + })(this)); + return parsed_entry; + }, + in_range: function(date, date_range) { + var ref; + if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { + return true; + } else { + return false; + } + } +}; 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', ], -- cgit v1.2.1 From 15a4773e06d91d57a80699818a7754e92e276b39 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 16 Feb 2018 16:49:13 -0600 Subject: 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'], -- cgit v1.2.1 From 91bbd80f493236dbcaf206183b55009a810d31d9 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 19 Feb 2018 17:04:08 -0600 Subject: fix broken specs --- .../pages/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 () { -- cgit v1.2.1 From c8fb31f72436e9d88809547c60ba8da0b081061d Mon Sep 17 00:00:00 2001 From: Diana Stanley Date: Mon, 19 Feb 2018 16:01:23 -0800 Subject: add lines describing how to find user clone/fetch activity --- doc/administration/logs.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/administration/logs.md b/doc/administration/logs.md index debaa2330d0..1b42d7979ed 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -32,6 +32,8 @@ In this example, you can see this was a GET request for a specific issue. Notice 2. `view`: total time taken inside the Rails views 3. `db`: total time to retrieve data from the database +User clone/fetch activity using http transport appears in this log as `action: git_upload_pack`. + In addition, the log contains the IP address from which the request originated (`remote_ip`) as well as the user's ID (`user_id`), and username (`username`). @@ -157,6 +159,8 @@ I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. ``` +User clone/fetch activity using ssh transport appears in this log as `executing git command Date: Sun, 18 Feb 2018 22:17:23 -0500 Subject: Add missing pagination on the commit diff endpoint --- .../unreleased/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 -- cgit v1.2.1 From 2823d2707e9e1a1d728fc7ae3da16edf6b1ba4e7 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Fri, 16 Feb 2018 11:37:33 +1100 Subject: 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 +- changelogs/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 -- cgit v1.2.1 From 6ceb2cb3c0f84bd78161c01ed97efae079c8a3ba Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 20 Feb 2018 17:32:25 +1100 Subject: 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')); + }); } } -- cgit v1.2.1 From a55106d629bde3d7d39ca7ec126f955e9a08b7bd Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Tue, 20 Feb 2018 01:48:06 -0500 Subject: Update Prometheus docs for enhanced integration --- .../integrations/img/prometheus_dashboard.png | Bin 0 -> 63234 bytes .../project/integrations/img/prometheus_deploy.png | Bin 0 -> 27258 bytes doc/user/project/integrations/prometheus.md | 128 +++++++-------------- .../integrations/prometheus_library/kubernetes.md | 9 +- .../project/integrations/samples/prometheus.yml | 107 ----------------- 5 files changed, 45 insertions(+), 199 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 00000000000..84eac5eb939 Binary files /dev/null and b/doc/user/project/integrations/img/prometheus_dashboard.png differ diff --git a/doc/user/project/integrations/img/prometheus_deploy.png b/doc/user/project/integrations/img/prometheus_deploy.png new file mode 100644 index 00000000000..d39081bcc7b Binary files /dev/null and b/doc/user/project/integrations/img/prometheus_deploy.png differ 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,119 +2,71 @@ > [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). -Integration with Prometheus requires the following: - -1. GitLab 9.0 or higher -1. Prometheus must be configured to collect one of the [supported metrics](prometheus_library/metrics.md) -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 - -Installing and configuring Prometheus to monitor applications is fairly straight forward. - -1. [Install Prometheus](https://prometheus.io/docs/introduction/install/) -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 +## Managed Prometheus on Kubernetes -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/#). +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. -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). +### Requirements -### Configuring your own Prometheus server within Kubernetes +* 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) -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. +### Getting started -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. +Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click. -![Deploy Prometheus](img/prometheus_yaml_deploy.png) +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 -Or use `kubectl`: +![Managed Prometheus Deploy](img/prometheus_deploy.png) -```bash -kubectl apply -f path/to/prometheus.yml -``` +### About managed Prometheus deployments -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). +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/). -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. +The Prometheus server will [automatically detect and monitor](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#%3Ckubernetes_sd_config%3E) nodes, pods, and endpoints. -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: +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`. -```bash -kubectl describe pods -n prometheus -``` +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. -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. +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. -![GCP Node Detail](img/prometheus_gcp_node_name.png) +## Manual configuration of Prometheus -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**. +### Requirements -Create a new rule: +Integration with Prometheus requires the following: -- 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. +1. GitLab 9.0 or higher +1. Prometheus must be configured to collect one of the [supported metrics](prometheus_library/metrics.md) +1. Each metric must be have a label to indicate the environment +1. GitLab must have network connectivity to the Prometheus server -![GCP Firewall Rule](img/prometheus_gcp_firewall_rule.png) +### Getting started ---- +Installing and configuring Prometheus to monitor applications is fairly straight forward. -Now that Prometheus is configured, proceed to -[configure the Prometheus project service in GitLab](##configuration-in-gitlab). +1. [Install Prometheus](https://prometheus.io/docs/introduction/install/) +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) -## 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 -- cgit v1.2.1 From c7fd85a7790ac821ce5cdaee245fae7e6b9f8d5a Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Tue, 20 Feb 2018 01:56:40 -0500 Subject: Fix links --- doc/user/project/integrations/prometheus.md | 10 +++++----- doc/user/project/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. -- cgit v1.2.1 From 7c9070a96523c8746d4945cc2029c15f93b0e82b Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Tue, 20 Feb 2018 02:08:57 -0500 Subject: 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 84eac5eb939..bd19f1b44cc 100644 Binary files a/doc/user/project/integrations/img/prometheus_dashboard.png and b/doc/user/project/integrations/img/prometheus_dashboard.png differ diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 8ddebf89c76..5d0d58eaf80 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -9,12 +9,12 @@ within the GitLab interface. ![Environment Dashboard](img/prometheus_dashboard.png) 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 deployments on Kubernetes, GitLab can automatically [deploy and manage Prometheus](#managed-prometheus-on-kubernetes) * 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.md), 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 of your apps easy. ### Requirements @@ -36,14 +36,12 @@ Once you have a connected Kubernetes cluster with Helm installed, deploying a ma 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/): +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. +CPU and Memory consumption is 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.md#installing-applications) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates. -- cgit v1.2.1 From fff70a04e1444d108ce02c0370ee85a0ecb07817 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 20 Feb 2018 10:02:11 +0100 Subject: 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: -- cgit v1.2.1 From bb9e7a3f2cf80c09b64156988abb9ffba1de6d2c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 16 Feb 2018 12:07:04 +0000 Subject: 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 +- .../pipelines/pipeline_details_bundle.js | 10 ++-- .../pipelines/pipeline_details_mediatior.js | 58 --------------------- .../pipelines/pipeline_details_mediator.js | 59 ++++++++++++++++++++++ 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 - 8 files changed, 80 insertions(+), 73 deletions(-) delete mode 100644 app/assets/javascripts/pipelines/pipeline_details_mediatior.js create mode 100644 app/assets/javascripts/pipelines/pipeline_details_mediator.js 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 @@