From 20037e61122a688366060f9427506962048e77ed Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 7 Jul 2016 21:03:21 +0800 Subject: [PATCH 001/141] Introduce Project#builds_for(build_name, ref = 'HEAD'): So that we could find the particular builds according to build_name and ref. It would be used to find the latest build artifacts from a particular branch or tag. --- app/models/commit_status.rb | 8 +++++++- app/models/project.rb | 8 ++++++++ spec/models/build_spec.rb | 12 ++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index e437e3417a8..6828705dbc8 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -16,7 +16,13 @@ class CommitStatus < ActiveRecord::Base alias_attribute :author, :user - scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } + scope :latest, -> do + id = unscope(:select). + select("max(#{table_name}.id)"). + group(:name, :commit_id) + + where(id: id) + end scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } diff --git a/app/models/project.rb b/app/models/project.rb index 029026a4e56..293dbd52359 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -429,6 +429,14 @@ class Project < ActiveRecord::Base repository.commit(id) end + def builds_for(build_name, ref = 'HEAD') + sha = commit(ref).sha + + builds.joins(:pipeline). + merge(Ci::Pipeline.where(sha: sha)). + where(name: build_name) + end + def merge_base_commit(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id) repository.commit(sha) if sha diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index e8171788872..8e3c9672fd5 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -673,7 +673,7 @@ describe Ci::Build, models: true do context 'when build is running' do before { build.run! } - it 'should return false' do + it 'returns false' do expect(build.retryable?).to be false end end @@ -681,9 +681,17 @@ describe Ci::Build, models: true do context 'when build is finished' do before { build.success! } - it 'should return true' do + it 'returns true' do expect(build.retryable?).to be true end end end + + describe 'Project#builds_for' do + it 'returns builds from ref and build name' do + latest_build = project.builds_for(build.name, 'HEAD').latest.first + + expect(latest_build.id).to eq(build.id) + end + end end From 1e3ff09cf3cd78755e83288559cfb1cf0ff6539f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 7 Jul 2016 21:18:10 +0800 Subject: [PATCH 002/141] Avoid ambiguous syntax --- spec/models/build_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 8e3c9672fd5..cb432a99cd2 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -674,7 +674,7 @@ describe Ci::Build, models: true do before { build.run! } it 'returns false' do - expect(build.retryable?).to be false + expect(build.retryable?).to be(false) end end @@ -682,7 +682,7 @@ describe Ci::Build, models: true do before { build.success! } it 'returns true' do - expect(build.retryable?).to be true + expect(build.retryable?).to be(true) end end end From 8f469c33cc8b90e1bcae8ddd5599ce2a2957a3af Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 7 Jul 2016 21:18:54 +0800 Subject: [PATCH 003/141] Multiline for before block --- spec/models/build_spec.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index cb432a99cd2..47ba4931460 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -671,7 +671,9 @@ describe Ci::Build, models: true do describe '#retryable?' do context 'when build is running' do - before { build.run! } + before do + build.run! + end it 'returns false' do expect(build.retryable?).to be(false) @@ -679,7 +681,9 @@ describe Ci::Build, models: true do end context 'when build is finished' do - before { build.success! } + before do + build.success! + end it 'returns true' do expect(build.retryable?).to be(true) From f601ec54fcfad7f365d3488c0a48575862c48958 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 11 Jul 2016 18:17:32 +0800 Subject: [PATCH 004/141] Introduce Projects::ArtifactsController#search: So we redirect from ref and build_name to the particular build, namely: * /u/r/artifacts/ref/build_name/* -> /u/r/builds/:build_id/artifacts/* For: * download * browse * file --- .../projects/artifacts_controller.rb | 24 +++++++++++++++++-- config/routes.rb | 6 +++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f11c8321464..c00295cd3b5 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -35,14 +35,34 @@ class Projects::ArtifactsController < Projects::ApplicationController redirect_to namespace_project_build_path(project.namespace, project, build) end + def search + url = namespace_project_build_url(project.namespace, project, build) + + if params[:path] + redirect_to "#{url}/artifacts/#{params[:path]}" + else + render_404 + end + end + private def validate_artifacts! - render_404 unless build.artifacts? + render_404 unless build && build.artifacts? end def build - @build ||= project.builds.find_by!(id: params[:build_id]) + @build ||= build_from_id || build_from_ref + end + + def build_from_id + project.builds.find_by(id: params[:build_id]) if params[:build_id] + end + + def build_from_ref + if params[:ref] + project.builds_for(params[:build_name], params[:ref]).latest.first + end end def artifacts_file diff --git a/config/routes.rb b/config/routes.rb index 1572656b8c5..0a4b8609252 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -733,6 +733,12 @@ Rails.application.routes.draw do resources :environments, only: [:index, :show, :new, :create, :destroy] + resources :artifacts, only: [] do + collection do + get :search, path: ':ref/:build_name(/*path)', format: false + end + end + resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all From b14d40f0b0b46aba95d15b139345674c6a3dbd09 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 11 Jul 2016 18:40:45 +0800 Subject: [PATCH 005/141] Handle branches with / in the name --- config/routes.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 0a4b8609252..5c1460b0e75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -735,7 +735,8 @@ Rails.application.routes.draw do resources :artifacts, only: [] do collection do - get :search, path: ':ref/:build_name(/*path)', format: false + get :search, path: ':ref/:build_name/*path', format: false, + constraints: { ref: %r{.+(?=/)} } # ref could have / end end From ef833a220508f6f8a692b74e7fe593c68981d6f5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 11 Jul 2016 18:51:23 +0800 Subject: [PATCH 006/141] Give latest succeeded one, don't give pending/running ones --- app/controllers/projects/artifacts_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index c00295cd3b5..f71499be4f7 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -61,7 +61,9 @@ class Projects::ArtifactsController < Projects::ApplicationController def build_from_ref if params[:ref] - project.builds_for(params[:build_name], params[:ref]).latest.first + builds = project.builds_for(params[:build_name], params[:ref]) + + builds.latest.success.first end end From df5b78676e914ad8e14e96322b1dce383ae26875 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 11 Jul 2016 19:06:14 +0800 Subject: [PATCH 007/141] Using plain if/else is much easier to understand --- app/controllers/projects/artifacts_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f71499be4f7..944fde11b09 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -5,11 +5,11 @@ class Projects::ArtifactsController < Projects::ApplicationController before_action :validate_artifacts! def download - unless artifacts_file.file_storage? - return redirect_to artifacts_file.url + if artifacts_file.file_storage? + send_file artifacts_file.path, disposition: 'attachment' + else + redirect_to artifacts_file.url end - - send_file artifacts_file.path, disposition: 'attachment' end def browse From 1f733a95c73ca767287fcad180e4fa367b4a2354 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 11 Jul 2016 19:06:40 +0800 Subject: [PATCH 008/141] Remove redundant return --- app/controllers/projects/artifacts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 944fde11b09..7d6ba80b965 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -16,7 +16,7 @@ class Projects::ArtifactsController < Projects::ApplicationController directory = params[:path] ? "#{params[:path]}/" : '' @entry = build.artifacts_metadata_entry(directory) - return render_404 unless @entry.exists? + render_404 unless @entry.exists? end def file From 2c646bb22593dc377c278622b35f79f1063725ad Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 11 Jul 2016 19:47:45 +0800 Subject: [PATCH 009/141] Move tests to respect to modules and extract artifacts tests --- spec/features/projects/artifacts_spec.rb | 31 +++++++++++++++++++++ spec/features/{ => projects}/builds_spec.rb | 17 ----------- 2 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 spec/features/projects/artifacts_spec.rb rename spec/features/{ => projects}/builds_spec.rb (93%) diff --git a/spec/features/projects/artifacts_spec.rb b/spec/features/projects/artifacts_spec.rb new file mode 100644 index 00000000000..fc6e2b07d40 --- /dev/null +++ b/spec/features/projects/artifacts_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'Artifacts' do + let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + + before do + login_as(:user) + @commit = FactoryGirl.create :ci_pipeline + @build = FactoryGirl.create :ci_build, pipeline: @commit + @build2 = FactoryGirl.create :ci_build + @project = @commit.project + @project.team << [@user, :developer] + end + + describe "GET /:project/builds/:id/artifacts/download" do + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Download' + end + + context "Build from other project" do + before do + @build2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end + end +end diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb similarity index 93% rename from spec/features/builds_spec.rb rename to spec/features/projects/builds_spec.rb index 16832c297ac..25689f1c6e8 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -196,23 +196,6 @@ describe "Builds" do end end - describe "GET /:project/builds/:id/download" do - before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@project.namespace, @project, @build) - click_link 'Download' - end - - context "Build from other project" do - before do - @build2.update_attributes(artifacts_file: artifacts_file) - visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) - end - - it { expect(page.status_code).to eq(404) } - end - end - describe "GET /:project/builds/:id/raw" do context "Build from project" do before do From d0451a050d5c4a3d343077d0820451af5058636b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 18:56:41 +0800 Subject: [PATCH 010/141] Test for Project#builds_for and return empty array for nothing --- app/models/project.rb | 3 +++ spec/models/project_spec.rb | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index f3266a1b197..35ffb0a415d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -430,6 +430,9 @@ class Project < ActiveRecord::Base end def builds_for(build_name, ref = 'HEAD') + ct = commit(ref) + return [] unless ct + sha = commit(ref).sha builds.joins(:pipeline). diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5a27ccbab0a..06d99240708 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -359,11 +359,25 @@ describe Project, models: true do describe :repository do let(:project) { create(:project) } - it 'should return valid repo' do + it 'returns valid repo' do expect(project.repository).to be_kind_of(Repository) end end + describe '#builds_for' do + let(:project) { create(:project) } + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit.sha) + end + let(:build) { create(:ci_build, pipeline: pipeline) } + + it 'returns builds for a particular ref' do + build_ids = project.builds_for(build.name, build.sha).map(&:id) + + expect(build_ids).to eq([build.id]) + end + end + describe :default_issues_tracker? do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } From 9604f12331ab0ce3a14b1780cc1245eb8df33038 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 19:14:44 +0800 Subject: [PATCH 011/141] Conforming the style --- spec/features/projects/artifacts_spec.rb | 28 ++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/spec/features/projects/artifacts_spec.rb b/spec/features/projects/artifacts_spec.rb index fc6e2b07d40..5e60c2f3074 100644 --- a/spec/features/projects/artifacts_spec.rb +++ b/spec/features/projects/artifacts_spec.rb @@ -2,27 +2,33 @@ require 'spec_helper' describe 'Artifacts' do let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:pipeline) { create(:ci_pipeline) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:build2) { create(:ci_build) } + let(:project) { pipeline.project } before do login_as(:user) - @commit = FactoryGirl.create :ci_pipeline - @build = FactoryGirl.create :ci_build, pipeline: @commit - @build2 = FactoryGirl.create :ci_build - @project = @commit.project - @project.team << [@user, :developer] + project.team << [@user, :developer] end - describe "GET /:project/builds/:id/artifacts/download" do + describe 'GET /:project/builds/:id/artifacts/download' do before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@project.namespace, @project, @build) + build.update_attributes(artifacts_file: artifacts_file) + + visit namespace_project_build_path(project.namespace, project, build) + click_link 'Download' end - context "Build from other project" do + context 'Build from other project' do before do - @build2.update_attributes(artifacts_file: artifacts_file) - visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) + build2.update_attributes(artifacts_file: artifacts_file) + + visit download_namespace_project_build_artifacts_path( + project.namespace, + project, + build2) end it { expect(page.status_code).to eq(404) } From a1eac5e4de95a4d27b30432c527ab410e9d77787 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 19:20:31 +0800 Subject: [PATCH 012/141] Doh. I already wrote that test and I forgot. --- spec/models/build_spec.rb | 4 ++-- spec/models/project_spec.rb | 14 -------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 47ba4931460..c7c247189f5 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -693,9 +693,9 @@ describe Ci::Build, models: true do describe 'Project#builds_for' do it 'returns builds from ref and build name' do - latest_build = project.builds_for(build.name, 'HEAD').latest.first + build_ids = project.builds_for(build.name, 'HEAD').map(&:id) - expect(latest_build.id).to eq(build.id) + expect(build_ids).to eq([build.id]) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 06d99240708..143fd5167a4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -364,20 +364,6 @@ describe Project, models: true do end end - describe '#builds_for' do - let(:project) { create(:project) } - let(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit.sha) - end - let(:build) { create(:ci_build, pipeline: pipeline) } - - it 'returns builds for a particular ref' do - build_ids = project.builds_for(build.name, build.sha).map(&:id) - - expect(build_ids).to eq([build.id]) - end - end - describe :default_issues_tracker? do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } From 6597c213c341ae072216c125a97f94a174fc3dfa Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 19:28:21 +0800 Subject: [PATCH 013/141] Prefer empty relation rather than arrays --- app/models/project.rb | 2 +- spec/models/build_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 35ffb0a415d..bc15f8c4138 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -431,7 +431,7 @@ class Project < ActiveRecord::Base def builds_for(build_name, ref = 'HEAD') ct = commit(ref) - return [] unless ct + return builds.none unless ct sha = commit(ref).sha diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index c7c247189f5..b1354faa722 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -697,5 +697,11 @@ describe Ci::Build, models: true do expect(build_ids).to eq([build.id]) end + + it 'returns empty relation if the build cannot be found' do + builds = project.builds_for(build.name, 'TAIL').all + + expect(builds).to be_empty + end end end From c94cff3e13d3f5ab55816ba2e84f48a659462441 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 19:29:59 +0800 Subject: [PATCH 014/141] Prefer if over return --- app/models/project.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index bc15f8c4138..366817079bb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -431,13 +431,17 @@ class Project < ActiveRecord::Base def builds_for(build_name, ref = 'HEAD') ct = commit(ref) - return builds.none unless ct - sha = commit(ref).sha + if ct.nil? + builds.none - builds.joins(:pipeline). - merge(Ci::Pipeline.where(sha: sha)). - where(name: build_name) + else + sha = ct.sha + + builds.joins(:pipeline). + merge(Ci::Pipeline.where(sha: sha)). + where(name: build_name) + end end def merge_base_commit(first_commit_id, second_commit_id) From a96e9aa0d3f703e61a415d2b0532f3a96d90ba51 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 20:17:55 +0800 Subject: [PATCH 015/141] Make rubocop happy --- spec/features/projects/artifacts_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/projects/artifacts_spec.rb b/spec/features/projects/artifacts_spec.rb index 5e60c2f3074..f5356a8b701 100644 --- a/spec/features/projects/artifacts_spec.rb +++ b/spec/features/projects/artifacts_spec.rb @@ -26,9 +26,9 @@ describe 'Artifacts' do build2.update_attributes(artifacts_file: artifacts_file) visit download_namespace_project_build_artifacts_path( - project.namespace, - project, - build2) + project.namespace, + project, + build2) end it { expect(page.status_code).to eq(404) } From e383254070baf8a4701e3a10b7cc699f03cefff4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 23:15:08 +0800 Subject: [PATCH 016/141] Add all the tests and fix stuffs along the way: It turns out they are different: builds.success.latest.first and builds.latest.success.first If we put success first, that latest would also filter via success, and which is what we want here. --- .../projects/artifacts_controller.rb | 2 +- config/routes.rb | 2 +- .../projects/artifacts_controller_spec.rb | 145 ++++++++++++++++++ spec/spec_helper.rb | 6 +- 4 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 spec/requests/projects/artifacts_controller_spec.rb diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 7d6ba80b965..25a1c2ca7e1 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -63,7 +63,7 @@ class Projects::ArtifactsController < Projects::ApplicationController if params[:ref] builds = project.builds_for(params[:build_name], params[:ref]) - builds.latest.success.first + builds.success.latest.first end end diff --git a/config/routes.rb b/config/routes.rb index 10b497d05a0..32b00652bb3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -736,7 +736,7 @@ Rails.application.routes.draw do resources :artifacts, only: [] do collection do get :search, path: ':ref/:build_name/*path', format: false, - constraints: { ref: %r{.+(?=/)} } # ref could have / + constraints: { ref: /.+/ } # ref could have / end end diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb new file mode 100644 index 00000000000..b11eb1aedcc --- /dev/null +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe Projects::ArtifactsController do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit('fix').sha) + end + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + before do + login_as(user) + project.team << [user, :developer] + end + + describe 'GET /:project/artifacts/:ref/:build_name/browse' do + context '404' do + it 'has no such ref' do + path = search_namespace_project_artifacts_path( + project.namespace, + project, + 'TAIL', + build.name, + 'browse') + + get path + + expect(response.status).to eq(404) + end + + it 'has no such build' do + get search_namespace_project_artifacts_path( + project.namespace, + project, + pipeline.sha, + 'NOBUILD', + 'browse') + + expect(response.status).to eq(404) + end + + it 'has no path' do + get search_namespace_project_artifacts_path( + project.namespace, + project, + pipeline.sha, + build.name, + '') + + expect(response.status).to eq(404) + end + end + + context '302' do + def path_from_sha + search_namespace_project_artifacts_path( + project.namespace, + project, + pipeline.sha, + build.name, + 'browse') + end + + shared_examples 'redirect to the build' do + it 'redirects' do + path = browse_namespace_project_build_artifacts_path( + project.namespace, + project, + build) + + expect(response).to redirect_to(path) + end + end + + context 'with sha' do + before do + get path_from_sha + end + + it_behaves_like 'redirect to the build' + end + + context 'with regular branch' do + before do + pipeline.update(sha: project.commit('master').sha) + end + + before do + get search_namespace_project_artifacts_path( + project.namespace, + project, + 'master', + build.name, + 'browse') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name containing slash' do + before do + pipeline.update(sha: project.commit('improve/awesome').sha) + end + + before do + get search_namespace_project_artifacts_path( + project.namespace, + project, + 'improve/awesome', + build.name, + 'browse') + end + + it_behaves_like 'redirect to the build' + end + + context 'with latest build' do + before do + 3.times do # creating some old builds + create(:ci_build, :success, :artifacts, pipeline: pipeline) + end + end + + before do + get path_from_sha + end + + it_behaves_like 'redirect to the build' + end + + context 'with success build' do + before do + build # make sure build was old, but still the latest success one + create(:ci_build, :pending, :artifacts, pipeline: pipeline) + end + + before do + get path_from_sha + end + + it_behaves_like 'redirect to the build' + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 606da1b7605..62097de2768 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,9 +28,9 @@ RSpec.configure do |config| config.verbose_retry = true config.display_try_failure_messages = true - config.include Devise::TestHelpers, type: :controller - config.include LoginHelpers, type: :feature - config.include LoginHelpers, type: :request + config.include Devise::TestHelpers, type: :controller + config.include Warden::Test::Helpers, type: :request + config.include LoginHelpers, type: :feature config.include StubConfiguration config.include EmailHelpers config.include RelativeUrl, type: feature From 57c72cb0dfc980106163b313f850c5b8a5e31a70 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 23:26:30 +0800 Subject: [PATCH 017/141] Could be faster when params[:path] is missing --- app/controllers/projects/artifacts_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 25a1c2ca7e1..bf9d72ae90c 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -36,9 +36,9 @@ class Projects::ArtifactsController < Projects::ApplicationController end def search - url = namespace_project_build_url(project.namespace, project, build) - if params[:path] + url = namespace_project_build_url(project.namespace, project, build) + redirect_to "#{url}/artifacts/#{params[:path]}" else render_404 From d0b9112fefcf0ee01d9df2dd9c2f1076738a53f1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 12 Jul 2016 23:28:41 +0800 Subject: [PATCH 018/141] save some lines and a local variable --- app/models/project.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 366817079bb..2a9d68d10d2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -436,10 +436,8 @@ class Project < ActiveRecord::Base builds.none else - sha = ct.sha - builds.joins(:pipeline). - merge(Ci::Pipeline.where(sha: sha)). + merge(Ci::Pipeline.where(sha: ct.sha)). where(name: build_name) end end From 3336828152767bf22f9c59d54c1c13f5e3d88132 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 13 Jul 2016 17:49:42 +0800 Subject: [PATCH 019/141] No need for a separate line now --- spec/requests/projects/artifacts_controller_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index b11eb1aedcc..2ab1c1ac428 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -16,15 +16,13 @@ describe Projects::ArtifactsController do describe 'GET /:project/artifacts/:ref/:build_name/browse' do context '404' do it 'has no such ref' do - path = search_namespace_project_artifacts_path( + get search_namespace_project_artifacts_path( project.namespace, project, 'TAIL', build.name, 'browse') - get path - expect(response.status).to eq(404) end From bb5f06718cd4fb344398a5fef19f3cf9b400de97 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 13 Jul 2016 18:19:29 +0800 Subject: [PATCH 020/141] Rename to ref_name so it's aligning with API --- app/controllers/projects/artifacts_controller.rb | 4 ++-- config/routes.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index bf9d72ae90c..f1370efd64e 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -60,8 +60,8 @@ class Projects::ArtifactsController < Projects::ApplicationController end def build_from_ref - if params[:ref] - builds = project.builds_for(params[:build_name], params[:ref]) + if params[:ref_name] + builds = project.builds_for(params[:build_name], params[:ref_name]) builds.success.latest.first end diff --git a/config/routes.rb b/config/routes.rb index 32b00652bb3..203f679226e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -735,8 +735,8 @@ Rails.application.routes.draw do resources :artifacts, only: [] do collection do - get :search, path: ':ref/:build_name/*path', format: false, - constraints: { ref: /.+/ } # ref could have / + get :search, path: ':ref_name/:build_name/*path', format: false, + constraints: { ref_name: /.+/ } # ref could have / end end From 6c80b597f58aaaca514e45a7e83b811db301e651 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 13 Jul 2016 21:44:27 +0800 Subject: [PATCH 021/141] Introduce path_from_ref and save some typing --- .../projects/artifacts_controller_spec.rb | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 2ab1c1ac428..4c4bacfcbda 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -50,12 +50,12 @@ describe Projects::ArtifactsController do end context '302' do - def path_from_sha + def path_from_ref(ref = pipeline.sha, build_name = build.name) search_namespace_project_artifacts_path( project.namespace, project, - pipeline.sha, - build.name, + ref, + build_name, 'browse') end @@ -72,7 +72,7 @@ describe Projects::ArtifactsController do context 'with sha' do before do - get path_from_sha + get path_from_ref end it_behaves_like 'redirect to the build' @@ -84,12 +84,7 @@ describe Projects::ArtifactsController do end before do - get search_namespace_project_artifacts_path( - project.namespace, - project, - 'master', - build.name, - 'browse') + get path_from_ref('master') end it_behaves_like 'redirect to the build' @@ -101,12 +96,7 @@ describe Projects::ArtifactsController do end before do - get search_namespace_project_artifacts_path( - project.namespace, - project, - 'improve/awesome', - build.name, - 'browse') + get path_from_ref('improve/awesome') end it_behaves_like 'redirect to the build' @@ -120,7 +110,7 @@ describe Projects::ArtifactsController do end before do - get path_from_sha + get path_from_ref end it_behaves_like 'redirect to the build' @@ -133,7 +123,7 @@ describe Projects::ArtifactsController do end before do - get path_from_sha + get path_from_ref end it_behaves_like 'redirect to the build' From 8735d95af16a6066e9f256a62f401d02c2c7e108 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 13 Jul 2016 23:05:57 +0800 Subject: [PATCH 022/141] Implement API for downloading artifacts from ref and build name: Basically: GET /api/projects/:id/artifacts/:ref_name/:build_name Also added tests for it. --- lib/api/api.rb | 1 + lib/api/artifacts.rb | 34 +++++ spec/requests/api/artifacts_spec.rb | 52 ++++++++ .../projects/artifacts_controller_spec.rb | 124 ++++-------------- spec/requests/shared/artifacts_context.rb | 78 +++++++++++ 5 files changed, 189 insertions(+), 100 deletions(-) create mode 100644 lib/api/artifacts.rb create mode 100644 spec/requests/api/artifacts_spec.rb create mode 100644 spec/requests/shared/artifacts_context.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index 3d7d67510a8..f18258bf5a3 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -26,6 +26,7 @@ module API # Ensure the namespace is right, otherwise we might load Grape::API::Helpers helpers ::API::Helpers + mount ::API::Artifacts mount ::API::AwardEmoji mount ::API::Branches mount ::API::Builds diff --git a/lib/api/artifacts.rb b/lib/api/artifacts.rb new file mode 100644 index 00000000000..6ce2bed8260 --- /dev/null +++ b/lib/api/artifacts.rb @@ -0,0 +1,34 @@ +module API + # Projects artifacts API + class Artifacts < Grape::API + before do + authenticate! + authorize!(:read_build, user_project) + end + + resource :projects do + # Download the artifacts file from ref_name and build_name + # + # Parameters: + # id (required) - The ID of a project + # ref_name (required) - The ref from repository + # build_name (required) - The name for the build + # Example Request: + # GET /projects/:id/artifacts/:ref_name/:build_name + get ':id/artifacts/:ref_name/:build_name', + requirements: { ref_name: /.+/ } do + builds = user_project.builds_for( + params[:build_name], params[:ref_name]) + + latest_build = builds.success.latest.first + + if latest_build + redirect( + "/projects/#{user_project.id}/builds/#{latest_build.id}/artifacts") + else + not_found! + end + end + end + end +end diff --git a/spec/requests/api/artifacts_spec.rb b/spec/requests/api/artifacts_spec.rb new file mode 100644 index 00000000000..2b84c1a2072 --- /dev/null +++ b/spec/requests/api/artifacts_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' +require_relative '../shared/artifacts_context' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit('fix').sha) + end + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + before do + project.team << [user, :developer] + end + + describe 'GET /projects/:id/artifacts/:ref_name/:build_name' do + def path_from_ref(ref = pipeline.sha, build_name = build.name, _ = '') + api("/projects/#{project.id}/artifacts/#{ref}/#{build_name}", user) + end + + context '401' do + let(:user) { nil } + + before do + get path_from_ref + end + + it 'gives 401 for unauthorized user' do + expect(response).to have_http_status(401) + end + end + + context '404' do + def verify + expect(response).to have_http_status(404) + end + + it_behaves_like 'artifacts from ref with 404' + end + + context '302' do + def verify + expect(response).to redirect_to( + "/projects/#{project.id}/builds/#{build.id}/artifacts") + end + + it_behaves_like 'artifacts from ref with 302' + end + end +end diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 4c4bacfcbda..9722a9a1d64 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require_relative '../shared/artifacts_context' describe Projects::ArtifactsController do let(:user) { create(:user) } @@ -14,120 +15,43 @@ describe Projects::ArtifactsController do end describe 'GET /:project/artifacts/:ref/:build_name/browse' do + def path_from_ref(ref = pipeline.sha, build_name = build.name, + path = 'browse') + search_namespace_project_artifacts_path( + project.namespace, + project, + ref, + build_name, + path) + end + context '404' do - it 'has no such ref' do - get search_namespace_project_artifacts_path( - project.namespace, - project, - 'TAIL', - build.name, - 'browse') - + def verify expect(response.status).to eq(404) end - it 'has no such build' do - get search_namespace_project_artifacts_path( - project.namespace, - project, - pipeline.sha, - 'NOBUILD', - 'browse') + it_behaves_like 'artifacts from ref with 404' - expect(response.status).to eq(404) - end + context 'has no path' do + before do + get path_from_ref(pipeline.sha, build.name, '') + end - it 'has no path' do - get search_namespace_project_artifacts_path( - project.namespace, - project, - pipeline.sha, - build.name, - '') - - expect(response.status).to eq(404) + it('gives 404') { verify } end end context '302' do - def path_from_ref(ref = pipeline.sha, build_name = build.name) - search_namespace_project_artifacts_path( + def verify + path = browse_namespace_project_build_artifacts_path( project.namespace, project, - ref, - build_name, - 'browse') + build) + + expect(response).to redirect_to(path) end - shared_examples 'redirect to the build' do - it 'redirects' do - path = browse_namespace_project_build_artifacts_path( - project.namespace, - project, - build) - - expect(response).to redirect_to(path) - end - end - - context 'with sha' do - before do - get path_from_ref - end - - it_behaves_like 'redirect to the build' - end - - context 'with regular branch' do - before do - pipeline.update(sha: project.commit('master').sha) - end - - before do - get path_from_ref('master') - end - - it_behaves_like 'redirect to the build' - end - - context 'with branch name containing slash' do - before do - pipeline.update(sha: project.commit('improve/awesome').sha) - end - - before do - get path_from_ref('improve/awesome') - end - - it_behaves_like 'redirect to the build' - end - - context 'with latest build' do - before do - 3.times do # creating some old builds - create(:ci_build, :success, :artifacts, pipeline: pipeline) - end - end - - before do - get path_from_ref - end - - it_behaves_like 'redirect to the build' - end - - context 'with success build' do - before do - build # make sure build was old, but still the latest success one - create(:ci_build, :pending, :artifacts, pipeline: pipeline) - end - - before do - get path_from_ref - end - - it_behaves_like 'redirect to the build' - end + it_behaves_like 'artifacts from ref with 302' end end end diff --git a/spec/requests/shared/artifacts_context.rb b/spec/requests/shared/artifacts_context.rb new file mode 100644 index 00000000000..4333be6e1cd --- /dev/null +++ b/spec/requests/shared/artifacts_context.rb @@ -0,0 +1,78 @@ +shared_context 'artifacts from ref with 404' do + context 'has no such ref' do + before do + get path_from_ref('TAIL', build.name) + end + + it('gives 404') { verify } + end + + context 'has no such build' do + before do + get path_from_ref(pipeline.sha, 'NOBUILD') + end + + it('gives 404') { verify } + end +end + +shared_context 'artifacts from ref with 302' do + context 'with sha' do + before do + get path_from_ref + end + + it('redirects') { verify } + end + + context 'with regular branch' do + before do + pipeline.update(sha: project.commit('master').sha) + end + + before do + get path_from_ref('master') + end + + it('redirects') { verify } + end + + context 'with branch name containing slash' do + before do + pipeline.update(sha: project.commit('improve/awesome').sha) + end + + before do + get path_from_ref('improve/awesome') + end + + it('redirects') { verify } + end + + context 'with latest build' do + before do + 3.times do # creating some old builds + create(:ci_build, :success, :artifacts, pipeline: pipeline) + end + end + + before do + get path_from_ref + end + + it('redirects') { verify } + end + + context 'with success build' do + before do + build # make sure build was old, but still the latest success one + create(:ci_build, :pending, :artifacts, pipeline: pipeline) + end + + before do + get path_from_ref + end + + it('redirects') { verify } + end +end From b043729d4959ab5cbfd4aff02ce8c8c4c8a9d26f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 13 Jul 2016 23:23:05 +0800 Subject: [PATCH 023/141] Share more stuffs --- spec/requests/api/artifacts_spec.rb | 13 ++----------- .../projects/artifacts_controller_spec.rb | 18 ++++++------------ spec/requests/shared/artifacts_context.rb | 17 +++++++++++++++-- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/spec/requests/api/artifacts_spec.rb b/spec/requests/api/artifacts_spec.rb index 2b84c1a2072..393b8f85402 100644 --- a/spec/requests/api/artifacts_spec.rb +++ b/spec/requests/api/artifacts_spec.rb @@ -4,18 +4,9 @@ require_relative '../shared/artifacts_context' describe API::API, api: true do include ApiHelpers - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit('fix').sha) - end - let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - - before do - project.team << [user, :developer] - end - describe 'GET /projects/:id/artifacts/:ref_name/:build_name' do + include_context 'artifacts from ref and build name' + def path_from_ref(ref = pipeline.sha, build_name = build.name, _ = '') api("/projects/#{project.id}/artifacts/#{ref}/#{build_name}", user) end diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 9722a9a1d64..26f6ee8d252 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -2,19 +2,13 @@ require 'spec_helper' require_relative '../shared/artifacts_context' describe Projects::ArtifactsController do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit('fix').sha) - end - let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - - before do - login_as(user) - project.team << [user, :developer] - end - describe 'GET /:project/artifacts/:ref/:build_name/browse' do + include_context 'artifacts from ref and build name' + + before do + login_as(user) + end + def path_from_ref(ref = pipeline.sha, build_name = build.name, path = 'browse') search_namespace_project_artifacts_path( diff --git a/spec/requests/shared/artifacts_context.rb b/spec/requests/shared/artifacts_context.rb index 4333be6e1cd..03f7f248773 100644 --- a/spec/requests/shared/artifacts_context.rb +++ b/spec/requests/shared/artifacts_context.rb @@ -1,4 +1,17 @@ -shared_context 'artifacts from ref with 404' do +shared_context 'artifacts from ref and build name' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit('fix').sha) + end + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + before do + project.team << [user, :developer] + end +end + +shared_examples 'artifacts from ref with 404' do context 'has no such ref' do before do get path_from_ref('TAIL', build.name) @@ -16,7 +29,7 @@ shared_context 'artifacts from ref with 404' do end end -shared_context 'artifacts from ref with 302' do +shared_examples 'artifacts from ref with 302' do context 'with sha' do before do get path_from_ref From 5b227f351f8b3568e948d705fb1e2bb571cfdcdd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 13 Jul 2016 23:39:03 +0800 Subject: [PATCH 024/141] rubocop likes this better --- spec/requests/projects/artifacts_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 26f6ee8d252..d44901289d8 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -9,8 +9,8 @@ describe Projects::ArtifactsController do login_as(user) end - def path_from_ref(ref = pipeline.sha, build_name = build.name, - path = 'browse') + def path_from_ref( + ref = pipeline.sha, build_name = build.name, path = 'browse') search_namespace_project_artifacts_path( project.namespace, project, From 66b91ce9ac70025093d52247ecfaa3f47536809f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 14:24:17 +0800 Subject: [PATCH 025/141] Avoid shadowing CommitStatus#id, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13047163 --- app/models/commit_status.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 6828705dbc8..baabbd785cc 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -17,11 +17,11 @@ class CommitStatus < ActiveRecord::Base alias_attribute :author, :user scope :latest, -> do - id = unscope(:select). + max_id = unscope(:select). select("max(#{table_name}.id)"). group(:name, :commit_id) - where(id: id) + where(id: max_id) end scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } From 5544852825d637dfe24b53e93b1e95d21767783c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 14:25:07 +0800 Subject: [PATCH 026/141] Remove blank line between if/else clause, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13047184 --- app/models/project.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 2a9d68d10d2..48bb9743439 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -434,7 +434,6 @@ class Project < ActiveRecord::Base if ct.nil? builds.none - else builds.joins(:pipeline). merge(Ci::Pipeline.where(sha: ct.sha)). From fab8bc5313a56c5f22e56903de2cb9c86df79fe4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 14:26:04 +0800 Subject: [PATCH 027/141] More descriptive local variable, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13047190 --- app/models/project.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 48bb9743439..793cf2d70fb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -430,13 +430,13 @@ class Project < ActiveRecord::Base end def builds_for(build_name, ref = 'HEAD') - ct = commit(ref) + commit_object = commit(ref) - if ct.nil? + if commit_object.nil? builds.none else builds.joins(:pipeline). - merge(Ci::Pipeline.where(sha: ct.sha)). + merge(Ci::Pipeline.where(sha: commit_object.sha)). where(name: build_name) end end From 1c7871e92f679f65e5b5d065d7478dd2e77f9b77 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 14:39:26 +0800 Subject: [PATCH 028/141] Introduce get_build! so we could omit one early return --- lib/api/builds.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index d36047acd1f..f6e96ee7f3a 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -52,8 +52,7 @@ module API get ':id/builds/:build_id' do authorize_read_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) present build, with: Entities::Build, user_can_download_artifacts: can?(current_user, :read_build, user_project) @@ -69,8 +68,7 @@ module API get ':id/builds/:build_id/artifacts' do authorize_read_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) artifacts_file = build.artifacts_file @@ -97,8 +95,7 @@ module API get ':id/builds/:build_id/trace' do authorize_read_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" content_type 'text/plain' @@ -118,8 +115,7 @@ module API post ':id/builds/:build_id/cancel' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) build.cancel @@ -137,8 +133,7 @@ module API post ':id/builds/:build_id/retry' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) return forbidden!('Build is not retryable') unless build.retryable? build = Ci::Build.retry(build, current_user) @@ -157,8 +152,7 @@ module API post ':id/builds/:build_id/erase' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build + build = get_build!(params[:build_id]) return forbidden!('Build is not erasable!') unless build.erasable? build.erase(erased_by: current_user) @@ -176,8 +170,8 @@ module API post ':id/builds/:build_id/artifacts/keep' do authorize_update_builds! - build = get_build(params[:build_id]) - return not_found!(build) unless build && build.artifacts? + build = get_build!(params[:build_id]) + return not_found!(build) unless build.artifacts? build.keep_artifacts! @@ -192,6 +186,10 @@ module API user_project.builds.find_by(id: id.to_i) end + def get_build!(id) + get_build(id) || not_found! + end + def filter_builds(builds, scope) return builds if scope.nil? || scope.empty? From e01c421b911a46774f8c5be92d383d8da14750c3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 16:36:09 +0800 Subject: [PATCH 029/141] Prefer if so it's more clear what's going on --- lib/api/builds.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index f6e96ee7f3a..b3b28541382 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -69,16 +69,17 @@ module API authorize_read_builds! build = get_build!(params[:build_id]) - artifacts_file = build.artifacts_file - unless artifacts_file.file_storage? - return redirect_to build.artifacts_file.url + if !artifacts_file.file_storage? + redirect_to(build.artifacts_file.url) + + elsif artifacts_file.exists? + present_file!(artifacts_file.path, artifacts_file.filename) + + else + not_found! end - - return not_found! unless artifacts_file.exists? - - present_file!(artifacts_file.path, artifacts_file.filename) end # Get a trace of a specific build of a project From d7bbee7593ee54a9685c9eded00b121cca3913be Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 16:45:46 +0800 Subject: [PATCH 030/141] Update routes based on feedback from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13058785 And note that job/build_name could contain `/` --- app/controllers/projects/artifacts_controller.rb | 2 +- config/routes.rb | 15 ++++++++------- .../projects/artifacts_controller_spec.rb | 8 ++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f1370efd64e..3e487c24cbd 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -61,7 +61,7 @@ class Projects::ArtifactsController < Projects::ApplicationController def build_from_ref if params[:ref_name] - builds = project.builds_for(params[:build_name], params[:ref_name]) + builds = project.builds_for(params[:job], params[:ref_name]) builds.success.latest.first end diff --git a/config/routes.rb b/config/routes.rb index 203f679226e..ea6465038df 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -733,16 +733,17 @@ Rails.application.routes.draw do resources :environments, only: [:index, :show, :new, :create, :destroy] - resources :artifacts, only: [] do - collection do - get :search, path: ':ref_name/:build_name/*path', format: false, - constraints: { ref_name: /.+/ } # ref could have / - end - end - resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all + + resources :artifacts, only: [] do + collection do + get :search, path: ':ref_name/*path', + format: false, + constraints: { ref_name: /.+/ } # could have / + end + end end member do diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index d44901289d8..574b7028617 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require_relative '../shared/artifacts_context' describe Projects::ArtifactsController do - describe 'GET /:project/artifacts/:ref/:build_name/browse' do + describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do include_context 'artifacts from ref and build name' before do @@ -10,13 +10,13 @@ describe Projects::ArtifactsController do end def path_from_ref( - ref = pipeline.sha, build_name = build.name, path = 'browse') + ref = pipeline.sha, job = build.name, path = 'browse') search_namespace_project_artifacts_path( project.namespace, project, ref, - build_name, - path) + path, + job: job) end context '404' do From a9a8ceebcbe25cbe27bebe9fc63ab364b1dd41ee Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 17:35:54 +0800 Subject: [PATCH 031/141] Merge features/projects/artifacts_spec.rb back It doesn't make too much sense having this standalone --- spec/features/projects/artifacts_spec.rb | 37 ------------------------ spec/features/projects/builds_spec.rb | 17 +++++++++++ 2 files changed, 17 insertions(+), 37 deletions(-) delete mode 100644 spec/features/projects/artifacts_spec.rb diff --git a/spec/features/projects/artifacts_spec.rb b/spec/features/projects/artifacts_spec.rb deleted file mode 100644 index f5356a8b701..00000000000 --- a/spec/features/projects/artifacts_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe 'Artifacts' do - let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - let(:pipeline) { create(:ci_pipeline) } - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:build2) { create(:ci_build) } - let(:project) { pipeline.project } - - before do - login_as(:user) - project.team << [@user, :developer] - end - - describe 'GET /:project/builds/:id/artifacts/download' do - before do - build.update_attributes(artifacts_file: artifacts_file) - - visit namespace_project_build_path(project.namespace, project, build) - - click_link 'Download' - end - - context 'Build from other project' do - before do - build2.update_attributes(artifacts_file: artifacts_file) - - visit download_namespace_project_build_artifacts_path( - project.namespace, - project, - build2) - end - - it { expect(page.status_code).to eq(404) } - end - end -end diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 25689f1c6e8..16832c297ac 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -196,6 +196,23 @@ describe "Builds" do end end + describe "GET /:project/builds/:id/download" do + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Download' + end + + context "Build from other project" do + before do + @build2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end + end + describe "GET /:project/builds/:id/raw" do context "Build from project" do before do From 70f508f5d48a3541a430539d7f8b41dfa99127a1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 18:50:34 +0800 Subject: [PATCH 032/141] Serve artifacts from Builds --- lib/api/api.rb | 1 - lib/api/artifacts.rb | 34 ---------------------------- lib/api/builds.rb | 35 +++++++++++++++++++++++++---- spec/requests/api/artifacts_spec.rb | 6 ++--- spec/requests/api/builds_spec.rb | 2 +- 5 files changed, 35 insertions(+), 43 deletions(-) delete mode 100644 lib/api/artifacts.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index f18258bf5a3..3d7d67510a8 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -26,7 +26,6 @@ module API # Ensure the namespace is right, otherwise we might load Grape::API::Helpers helpers ::API::Helpers - mount ::API::Artifacts mount ::API::AwardEmoji mount ::API::Branches mount ::API::Builds diff --git a/lib/api/artifacts.rb b/lib/api/artifacts.rb deleted file mode 100644 index 6ce2bed8260..00000000000 --- a/lib/api/artifacts.rb +++ /dev/null @@ -1,34 +0,0 @@ -module API - # Projects artifacts API - class Artifacts < Grape::API - before do - authenticate! - authorize!(:read_build, user_project) - end - - resource :projects do - # Download the artifacts file from ref_name and build_name - # - # Parameters: - # id (required) - The ID of a project - # ref_name (required) - The ref from repository - # build_name (required) - The name for the build - # Example Request: - # GET /projects/:id/artifacts/:ref_name/:build_name - get ':id/artifacts/:ref_name/:build_name', - requirements: { ref_name: /.+/ } do - builds = user_project.builds_for( - params[:build_name], params[:ref_name]) - - latest_build = builds.success.latest.first - - if latest_build - redirect( - "/projects/#{user_project.id}/builds/#{latest_build.id}/artifacts") - else - not_found! - end - end - end - end -end diff --git a/lib/api/builds.rb b/lib/api/builds.rb index b3b28541382..505ba1a66fe 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -71,12 +71,27 @@ module API build = get_build!(params[:build_id]) artifacts_file = build.artifacts_file - if !artifacts_file.file_storage? - redirect_to(build.artifacts_file.url) + present_artifact!(artifacts_file) + end - elsif artifacts_file.exists? - present_file!(artifacts_file.path, artifacts_file.filename) + # Download the artifacts file from ref_name and build_name + # + # Parameters: + # id (required) - The ID of a project + # ref_name (required) - The ref from repository + # job (required) - The name for the build + # Example Request: + # GET /projects/:id/artifacts/:ref_name/:build_name + get ':id/builds/artifacts/:ref_name', + requirements: { ref_name: /.+/ } do + builds = user_project.builds_for( + params[:job], params[:ref_name]) + latest_build = builds.success.latest.first + + if latest_build + redirect( + "/projects/#{user_project.id}/builds/#{latest_build.id}/artifacts") else not_found! end @@ -191,6 +206,18 @@ module API get_build(id) || not_found! end + def present_artifact!(artifacts_file) + if !artifacts_file.file_storage? + redirect_to(build.artifacts_file.url) + + elsif artifacts_file.exists? + present_file!(artifacts_file.path, artifacts_file.filename) + + else + not_found! + end + end + def filter_builds(builds, scope) return builds if scope.nil? || scope.empty? diff --git a/spec/requests/api/artifacts_spec.rb b/spec/requests/api/artifacts_spec.rb index 393b8f85402..f1461f7bc53 100644 --- a/spec/requests/api/artifacts_spec.rb +++ b/spec/requests/api/artifacts_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' require_relative '../shared/artifacts_context' -describe API::API, api: true do +describe API::API, api: true do include ApiHelpers describe 'GET /projects/:id/artifacts/:ref_name/:build_name' do include_context 'artifacts from ref and build name' - def path_from_ref(ref = pipeline.sha, build_name = build.name, _ = '') - api("/projects/#{project.id}/artifacts/#{ref}/#{build_name}", user) + def path_from_ref(ref = pipeline.sha, job = build.name) + api("/projects/#{project.id}/builds/artifacts/#{ref}?job=#{job}", user) end context '401' do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index f5b39c3d698..b661bf71545 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API, api: true do +describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } From c4496de8bf90ded13245fedc6b760805f15f6942 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 14 Jul 2016 19:02:07 +0800 Subject: [PATCH 033/141] Provide the file directly rather than redirecting --- lib/api/builds.rb | 6 ++---- spec/requests/api/artifacts_spec.rb | 11 ++++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 505ba1a66fe..5c14444f91a 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -69,9 +69,8 @@ module API authorize_read_builds! build = get_build!(params[:build_id]) - artifacts_file = build.artifacts_file - present_artifact!(artifacts_file) + present_artifact!(build.artifacts_file) end # Download the artifacts file from ref_name and build_name @@ -90,8 +89,7 @@ module API latest_build = builds.success.latest.first if latest_build - redirect( - "/projects/#{user_project.id}/builds/#{latest_build.id}/artifacts") + present_artifact!(latest_build.artifacts_file) else not_found! end diff --git a/spec/requests/api/artifacts_spec.rb b/spec/requests/api/artifacts_spec.rb index f1461f7bc53..56d0965b0be 100644 --- a/spec/requests/api/artifacts_spec.rb +++ b/spec/requests/api/artifacts_spec.rb @@ -31,10 +31,15 @@ describe API::API, api: true do it_behaves_like 'artifacts from ref with 404' end - context '302' do + context '200' do def verify - expect(response).to redirect_to( - "/projects/#{project.id}/builds/#{build.id}/artifacts") + download_headers = + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{build.artifacts_file.filename}" } + + expect(response).to have_http_status(200) + expect(response.headers).to include(download_headers) end it_behaves_like 'artifacts from ref with 302' From 640fc8264d6adce67e89236e23cd83656a84c505 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 15 Jul 2016 00:53:27 +0800 Subject: [PATCH 034/141] Avoid using let! --- spec/requests/api/builds_spec.rb | 62 ++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index b661bf71545..350d9cb5c7e 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -6,16 +6,21 @@ describe API::API, api: true do let(:user) { create(:user) } let(:api_user) { user } let(:user2) { create(:user) } - let!(:project) { create(:project, creator_id: user.id) } - let!(:developer) { create(:project_member, :developer, user: user, project: project) } - let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } - let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } - let!(:build) { create(:ci_build, pipeline: pipeline) } + let(:project) { create(:project, creator_id: user.id) } + let(:developer) { create(:project_member, :developer, user: user, project: project) } + let(:reporter) { create(:project_member, :reporter, user: user2, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } + let(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do let(:query) { '' } - before { get api("/projects/#{project.id}/builds?#{query}", api_user) } + before do + developer + build + + get api("/projects/#{project.id}/builds?#{query}", api_user) + end context 'authorized user' do it 'should return project builds' do @@ -77,9 +82,9 @@ describe API::API, api: true do context 'when user is authorized' do context 'when pipeline has builds' do before do - create(:ci_pipeline, project: project, sha: project.commit.id) + developer + build create(:ci_build, pipeline: pipeline) - create(:ci_build) get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) end @@ -93,6 +98,8 @@ describe API::API, api: true do context 'when pipeline has no builds' do before do + developer + branch_head = project.commit('feature').id get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) end @@ -107,8 +114,7 @@ describe API::API, api: true do context 'when user is not authorized' do before do - create(:ci_pipeline, project: project, sha: project.commit.id) - create(:ci_build, pipeline: pipeline) + build get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) end @@ -122,7 +128,11 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id' do - before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } + before do + developer + + get api("/projects/#{project.id}/builds/#{build.id}", api_user) + end context 'authorized user' do it 'should return specific build data' do @@ -141,7 +151,11 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/artifacts' do - before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } + before do + developer + + get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) + end context 'build with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -175,7 +189,11 @@ describe API::API, api: true do describe 'GET /projects/:id/builds/:build_id/trace' do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } - before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } + before do + developer + + get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) + end context 'authorized user' do it 'should return specific build trace' do @@ -194,7 +212,12 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/cancel' do - before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } + before do + developer + reporter + + post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) + end context 'authorized user' do context 'user with :update_build persmission' do @@ -225,7 +248,12 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/retry' do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } - before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } + before do + developer + reporter + + post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) + end context 'authorized user' do context 'user with :update_build permission' do @@ -256,6 +284,8 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/erase' do before do + developer + post api("/projects/#{project.id}/builds/#{build.id}/erase", user) end @@ -286,6 +316,8 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do before do + developer + post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) end From 398de9f1de8d2c0bf1bba57a4737a4518975dc85 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 15 Jul 2016 01:07:41 +0800 Subject: [PATCH 035/141] now we're able to merge the spec --- spec/requests/api/artifacts_spec.rb | 48 ----------------------------- spec/requests/api/builds_spec.rb | 43 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 48 deletions(-) delete mode 100644 spec/requests/api/artifacts_spec.rb diff --git a/spec/requests/api/artifacts_spec.rb b/spec/requests/api/artifacts_spec.rb deleted file mode 100644 index 56d0965b0be..00000000000 --- a/spec/requests/api/artifacts_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' -require_relative '../shared/artifacts_context' - -describe API::API, api: true do - include ApiHelpers - - describe 'GET /projects/:id/artifacts/:ref_name/:build_name' do - include_context 'artifacts from ref and build name' - - def path_from_ref(ref = pipeline.sha, job = build.name) - api("/projects/#{project.id}/builds/artifacts/#{ref}?job=#{job}", user) - end - - context '401' do - let(:user) { nil } - - before do - get path_from_ref - end - - it 'gives 401 for unauthorized user' do - expect(response).to have_http_status(401) - end - end - - context '404' do - def verify - expect(response).to have_http_status(404) - end - - it_behaves_like 'artifacts from ref with 404' - end - - context '200' do - def verify - download_headers = - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => - "attachment; filename=#{build.artifacts_file.filename}" } - - expect(response).to have_http_status(200) - expect(response.headers).to include(download_headers) - end - - it_behaves_like 'artifacts from ref with 302' - end - end -end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 350d9cb5c7e..5d2e2293236 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require_relative '../shared/artifacts_context' describe API::API, api: true do include ApiHelpers @@ -186,6 +187,48 @@ describe API::API, api: true do end end + describe 'GET /projects/:id/artifacts/:ref_name/:build_name' do + include_context 'artifacts from ref and build name' + + def path_from_ref(ref = pipeline.sha, job = build.name) + api("/projects/#{project.id}/builds/artifacts/#{ref}?job=#{job}", user) + end + + context '401' do + let(:user) { nil } + + before do + get path_from_ref + end + + it 'gives 401 for unauthorized user' do + expect(response).to have_http_status(401) + end + end + + context '404' do + def verify + expect(response).to have_http_status(404) + end + + it_behaves_like 'artifacts from ref with 404' + end + + context '200' do + def verify + download_headers = + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{build.artifacts_file.filename}" } + + expect(response).to have_http_status(200) + expect(response.headers).to include(download_headers) + end + + it_behaves_like 'artifacts from ref with 302' + end + end + describe 'GET /projects/:id/builds/:build_id/trace' do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } From e96401f097e3d3bffe3a34d6e053af356109370b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 15 Jul 2016 01:24:18 +0800 Subject: [PATCH 036/141] Add a download prefix so that we could add file prefix in the future --- lib/api/builds.rb | 7 +++---- spec/requests/api/builds_spec.rb | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 5c14444f91a..6792afb064e 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -80,11 +80,10 @@ module API # ref_name (required) - The ref from repository # job (required) - The name for the build # Example Request: - # GET /projects/:id/artifacts/:ref_name/:build_name - get ':id/builds/artifacts/:ref_name', + # GET /projects/:id/artifacts/download/:ref_name?job=name + get ':id/builds/artifacts/download/:ref_name', requirements: { ref_name: /.+/ } do - builds = user_project.builds_for( - params[:job], params[:ref_name]) + builds = user_project.builds_for(params[:job], params[:ref_name]) latest_build = builds.success.latest.first diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 5d2e2293236..246d273073a 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -191,7 +191,9 @@ describe API::API, api: true do include_context 'artifacts from ref and build name' def path_from_ref(ref = pipeline.sha, job = build.name) - api("/projects/#{project.id}/builds/artifacts/#{ref}?job=#{job}", user) + api( + "/projects/#{project.id}/builds/artifacts/download/#{ref}?job=#{job}", + user) end context '401' do From 2e90abf254096253ac0201de48210c9cabdb2db4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 15 Jul 2016 01:45:04 +0800 Subject: [PATCH 037/141] It could be redirecting or downloading in Rails or API --- spec/requests/api/builds_spec.rb | 2 +- spec/requests/projects/artifacts_controller_spec.rb | 2 +- spec/requests/shared/artifacts_context.rb | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 246d273073a..d226646e439 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -227,7 +227,7 @@ describe API::API, api: true do expect(response.headers).to include(download_headers) end - it_behaves_like 'artifacts from ref with 302' + it_behaves_like 'artifacts from ref successfully' end end diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 574b7028617..c8a21f7d0ec 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -45,7 +45,7 @@ describe Projects::ArtifactsController do expect(response).to redirect_to(path) end - it_behaves_like 'artifacts from ref with 302' + it_behaves_like 'artifacts from ref successfully' end end end diff --git a/spec/requests/shared/artifacts_context.rb b/spec/requests/shared/artifacts_context.rb index 03f7f248773..0c9f33bfcb2 100644 --- a/spec/requests/shared/artifacts_context.rb +++ b/spec/requests/shared/artifacts_context.rb @@ -29,13 +29,13 @@ shared_examples 'artifacts from ref with 404' do end end -shared_examples 'artifacts from ref with 302' do +shared_examples 'artifacts from ref successfully' do context 'with sha' do before do get path_from_ref end - it('redirects') { verify } + it('gives the file') { verify } end context 'with regular branch' do @@ -47,7 +47,7 @@ shared_examples 'artifacts from ref with 302' do get path_from_ref('master') end - it('redirects') { verify } + it('gives the file') { verify } end context 'with branch name containing slash' do @@ -59,7 +59,7 @@ shared_examples 'artifacts from ref with 302' do get path_from_ref('improve/awesome') end - it('redirects') { verify } + it('gives the file') { verify } end context 'with latest build' do @@ -73,7 +73,7 @@ shared_examples 'artifacts from ref with 302' do get path_from_ref end - it('redirects') { verify } + it('gives the file') { verify } end context 'with success build' do @@ -86,6 +86,6 @@ shared_examples 'artifacts from ref with 302' do get path_from_ref end - it('redirects') { verify } + it('gives the file') { verify } end end From c23f2aa53099c6c906a8c6457183502450fa5703 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 15 Jul 2016 01:58:13 +0800 Subject: [PATCH 038/141] Fix outdated comment --- lib/api/builds.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 6792afb064e..e65dfa88746 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -73,7 +73,7 @@ module API present_artifact!(build.artifacts_file) end - # Download the artifacts file from ref_name and build_name + # Download the artifacts file from ref_name and job # # Parameters: # id (required) - The ID of a project From 4bb3787eee282434263c37194a443edf6a93c1b7 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 15 Jul 2016 02:01:10 +0800 Subject: [PATCH 039/141] Try to make the URL more consistent between Rails and API --- lib/api/builds.rb | 4 ++-- spec/requests/api/builds_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index e65dfa88746..237a88adcc7 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -80,8 +80,8 @@ module API # ref_name (required) - The ref from repository # job (required) - The name for the build # Example Request: - # GET /projects/:id/artifacts/download/:ref_name?job=name - get ':id/builds/artifacts/download/:ref_name', + # GET /projects/:id/artifacts/:ref_name/download?job=name + get ':id/builds/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do builds = user_project.builds_for(params[:job], params[:ref_name]) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index d226646e439..59ea7992023 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -187,12 +187,12 @@ describe API::API, api: true do end end - describe 'GET /projects/:id/artifacts/:ref_name/:build_name' do + describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do include_context 'artifacts from ref and build name' def path_from_ref(ref = pipeline.sha, job = build.name) api( - "/projects/#{project.id}/builds/artifacts/download/#{ref}?job=#{job}", + "/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", user) end From 53a9dee6cb54b75fa2999b4a33a59928b3b73ec3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 15 Jul 2016 02:22:29 +0800 Subject: [PATCH 040/141] Introduce Project#latest_success_builds_for: So it's more accessible for views to access the names of jobs. Only filter Build#name from where we really need to download it. --- app/controllers/projects/artifacts_controller.rb | 4 ++-- app/models/project.rb | 9 ++++++--- lib/api/builds.rb | 5 ++--- spec/models/build_spec.rb | 12 ++++++++---- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 3e487c24cbd..1b3c4ec9bd8 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -61,9 +61,9 @@ class Projects::ArtifactsController < Projects::ApplicationController def build_from_ref if params[:ref_name] - builds = project.builds_for(params[:job], params[:ref_name]) + builds = project.latest_success_builds_for(params[:ref_name]) - builds.success.latest.first + builds.where(name: params[:job]).first end end diff --git a/app/models/project.rb b/app/models/project.rb index 793cf2d70fb..384841dbb9a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -429,15 +429,18 @@ class Project < ActiveRecord::Base repository.commit(ref) end - def builds_for(build_name, ref = 'HEAD') + def latest_success_builds_for(ref = 'HEAD') + builds_for(ref).success.latest + end + + def builds_for(ref = 'HEAD') commit_object = commit(ref) if commit_object.nil? builds.none else builds.joins(:pipeline). - merge(Ci::Pipeline.where(sha: commit_object.sha)). - where(name: build_name) + merge(Ci::Pipeline.where(sha: commit_object.sha)) end end diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 237a88adcc7..53774b5c10f 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -83,9 +83,8 @@ module API # GET /projects/:id/artifacts/:ref_name/download?job=name get ':id/builds/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do - builds = user_project.builds_for(params[:job], params[:ref_name]) - - latest_build = builds.success.latest.first + builds = user_project.latest_success_builds_for(params[:ref_name]) + latest_build = builds.where(name: params[:job]).first if latest_build present_artifact!(latest_build.artifacts_file) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index b1354faa722..7c95463a571 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -691,15 +691,19 @@ describe Ci::Build, models: true do end end - describe 'Project#builds_for' do - it 'returns builds from ref and build name' do - build_ids = project.builds_for(build.name, 'HEAD').map(&:id) + describe 'Project#latest_success_builds_for' do + before do + build.update(status: 'success') + end + + it 'returns builds from ref' do + build_ids = project.latest_success_builds_for('HEAD').map(&:id) expect(build_ids).to eq([build.id]) end it 'returns empty relation if the build cannot be found' do - builds = project.builds_for(build.name, 'TAIL').all + builds = project.latest_success_builds_for('TAIL').all expect(builds).to be_empty end From e313fd124225a0d89f3ad86a0cbb62b93c855898 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Sat, 16 Jul 2016 17:44:34 -0500 Subject: [PATCH 041/141] Add artifacts button --- app/views/projects/tags/_download.html.haml | 26 +++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 8a11dbfa9f4..3f335b84c52 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -1,14 +1,32 @@ -%span.btn-group - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do - %span Source code - %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } +.dropdown.inline + %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + = icon('download') %span.caret %span.sr-only Select Archive Format %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do %span Download zip %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz + - job_names = project.latest_success_builds_for('v8.9.6').pluck(:name) + %li.dropdown-header Artifacts + + + +-# +-# - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } +-# - if artifacts.present? +-# .dropdown.inline.build-artifacts +-# %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} +-# = icon('download') +-# %b.caret +-# %ul.dropdown-menu.dropdown-menu-align-right +-# - artifacts.each do |build| +-# %li +-# = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do +-# = icon("download") +-# %span Download '#{build.name}' artifacts From faeaeda60e5601914338899f6b23b677d37a2ab5 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Sun, 17 Jul 2016 13:22:53 -0500 Subject: [PATCH 042/141] Add artifacts to tags --- app/views/projects/tags/_download.html.haml | 25 ++++++--------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 3f335b84c52..1f4c6c9ec08 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -12,21 +12,10 @@ %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - job_names = project.latest_success_builds_for('v8.9.6').pluck(:name) - %li.dropdown-header Artifacts - - - --# --# - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } --# - if artifacts.present? --# .dropdown.inline.build-artifacts --# %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} --# = icon('download') --# %b.caret --# %ul.dropdown-menu.dropdown-menu-align-right --# - artifacts.each do |build| --# %li --# = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do --# = icon("download") --# %span Download '#{build.name}' artifacts + - artifacts = project.builds_for(ref).latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - artifacts.each do |job| + %li + = link_to download_namespace_project_build_artifacts_path(project.namespace, project, job), rel: 'nofollow' do + %span Download '#{job.name}' From 5c28f16a01dd2c5f3e5b4e97d70ee3a9b0cdad3f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Sun, 17 Jul 2016 19:04:45 -0500 Subject: [PATCH 043/141] Add artifacts download button on project page and branches page --- app/assets/stylesheets/framework/lists.scss | 4 ++++ app/views/projects/branches/_branch.html.haml | 15 ++++++++++++ .../projects/buttons/_download.html.haml | 23 +++++++++++++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 2c40ec430ca..95a56ee0e95 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -157,6 +157,10 @@ ul.content-list { margin-right: 0; } } + + .artifacts-btn { + margin-right: 10px; + } } // When dragging a list item diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 4bd85061240..78b3de46f58 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,6 +27,21 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare + - artifacts = @project.builds_for(@repository.root_ref).latest.with_artifacts + - if artifacts.any? + .dropdown.inline.artifacts-btn + %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + = icon('download') + %span.caret + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Artifacts + - artifacts.each do |job| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do + %span Download '#{job.name}' + - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 58f43ecb5d5..c971420b16c 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,4 +1,23 @@ - unless @project.empty_repo? - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do - = icon('download') + .dropdown.inline.btn-group + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + %span.caret + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), rel: 'nofollow' do + %span Download zip + %li + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'tar.gz'), rel: 'nofollow' do + %span Download tar.gz + - artifacts = @project.builds_for(@ref).latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - artifacts.each do |job| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do + %span Download '#{job.name}' From 39c1cabf27da7a082f4e3da669da6b91393016d9 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Sun, 17 Jul 2016 19:11:00 -0500 Subject: [PATCH 044/141] Fix dropdown caret --- app/views/projects/buttons/_download.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index c971420b16c..f504d514963 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -3,7 +3,7 @@ .dropdown.inline.btn-group %button.btn{ 'data-toggle' => 'dropdown' } = icon('download') - %span.caret + = icon('caret-down') %span.sr-only Select Archive Format %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } From 5151ebf4c68b3ac87d51474b49962f0e5ba6d3e7 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 13:37:00 +0800 Subject: [PATCH 045/141] Use RSpec helpers, feedback from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125543 --- spec/models/build_spec.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 7c95463a571..b2dcbd8da2e 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -628,7 +628,7 @@ describe Ci::Build, models: true do describe '#erasable?' do subject { build.erasable? } - it { is_expected.to eq true } + it { is_expected.to be_truthy } end describe '#erased?' do @@ -636,7 +636,7 @@ describe Ci::Build, models: true do subject { build.erased? } context 'build has not been erased' do - it { is_expected.to be false } + it { is_expected.to be_falsey } end context 'build has been erased' do @@ -644,12 +644,13 @@ describe Ci::Build, models: true do build.erase end - it { is_expected.to be true } + it { is_expected.to be_truthy } end end context 'metadata and build trace are not available' do let!(:build) { create(:ci_build, :success, :artifacts) } + before do build.remove_artifacts_metadata! end @@ -676,7 +677,7 @@ describe Ci::Build, models: true do end it 'returns false' do - expect(build.retryable?).to be(false) + expect(build).not_to be_retryable end end @@ -686,7 +687,7 @@ describe Ci::Build, models: true do end it 'returns true' do - expect(build.retryable?).to be(true) + expect(build).to be_retryable end end end From 78c37bdd773da9a41dc55e6915408ccae03186ff Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 13:39:53 +0800 Subject: [PATCH 046/141] Use find_by, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125486 --- app/controllers/projects/artifacts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 1b3c4ec9bd8..bfe0781d735 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -63,7 +63,7 @@ class Projects::ArtifactsController < Projects::ApplicationController if params[:ref_name] builds = project.latest_success_builds_for(params[:ref_name]) - builds.where(name: params[:job]).first + builds.find_by(name: params[:job]) end end From 6dcb75f98515d8f3a723edc1900e80cf9427c486 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 13:41:37 +0800 Subject: [PATCH 047/141] Remove blank lines between clauses, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125597 --- lib/api/builds.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 53774b5c10f..d988c669cb1 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -205,10 +205,8 @@ module API def present_artifact!(artifacts_file) if !artifacts_file.file_storage? redirect_to(build.artifacts_file.url) - elsif artifacts_file.exists? present_file!(artifacts_file.path, artifacts_file.filename) - else not_found! end From 6de48227badd879f7e12803592daa2fc73656f91 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 13:58:48 +0800 Subject: [PATCH 048/141] Use single line even if they're more than 80 chars, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125628 --- spec/requests/api/builds_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 59ea7992023..bc6242a0d71 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -191,9 +191,7 @@ describe API::API, api: true do include_context 'artifacts from ref and build name' def path_from_ref(ref = pipeline.sha, job = build.name) - api( - "/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", - user) + api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", user) end context '401' do From 6830e2821f16d832963320aae571612f50a8aaa0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 14:00:39 +0800 Subject: [PATCH 049/141] Match against records rather than id, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125605 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125611 --- spec/models/build_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index b2dcbd8da2e..a57f0b6886c 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -698,9 +698,9 @@ describe Ci::Build, models: true do end it 'returns builds from ref' do - build_ids = project.latest_success_builds_for('HEAD').map(&:id) + builds = project.latest_success_builds_for('HEAD') - expect(build_ids).to eq([build.id]) + expect(builds).to contain_exactly(build) end it 'returns empty relation if the build cannot be found' do From cc91f09ac39a7c201d527734e835d01dc13e5059 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 14:34:03 +0800 Subject: [PATCH 050/141] Just use find_by! and we're rescuing ActiveRecord::RecordNotFound Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125645 --- lib/api/builds.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index d988c669cb1..a27397a82f7 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -84,13 +84,9 @@ module API get ':id/builds/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do builds = user_project.latest_success_builds_for(params[:ref_name]) - latest_build = builds.where(name: params[:job]).first + latest_build = builds.find_by!(name: params[:job]) - if latest_build - present_artifact!(latest_build.artifacts_file) - else - not_found! - end + present_artifact!(latest_build.artifacts_file) end # Get a trace of a specific build of a project From 5c1f75e983c88d4c884a15e9f84550fd256fb07f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 16:47:47 +0800 Subject: [PATCH 051/141] Use ci_commits.gl_project_id instead of ci_builds.gl_project_id: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13125513 --- app/models/project.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 384841dbb9a..d6e37e66a8b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -437,10 +437,11 @@ class Project < ActiveRecord::Base commit_object = commit(ref) if commit_object.nil? - builds.none + Ci::Build.none else - builds.joins(:pipeline). - merge(Ci::Pipeline.where(sha: commit_object.sha)) + Ci::Build.joins(:pipeline). + merge(Ci::Pipeline.where(sha: commit_object.sha, + project: self)) end end From 85409a5a10d22bebbc54a9c7b7c76e7c0e11b208 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 20:10:50 +0800 Subject: [PATCH 052/141] Use ci_commits.ref (Pipeline#ref) to find builds --- app/models/project.rb | 11 ++--------- spec/models/build_spec.rb | 5 +++-- spec/requests/api/builds_spec.rb | 2 +- .../projects/artifacts_controller_spec.rb | 2 +- spec/requests/shared/artifacts_context.rb | 19 ++++++++----------- 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index d6e37e66a8b..770ec1c8a68 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -434,15 +434,8 @@ class Project < ActiveRecord::Base end def builds_for(ref = 'HEAD') - commit_object = commit(ref) - - if commit_object.nil? - Ci::Build.none - else - Ci::Build.joins(:pipeline). - merge(Ci::Pipeline.where(sha: commit_object.sha, - project: self)) - end + Ci::Build.joins(:pipeline). + merge(Ci::Pipeline.where(ref: ref, project: self)) end def merge_base_commit(first_commit_id, second_commit_id) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index a57f0b6886c..53064138a50 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -5,7 +5,8 @@ describe Ci::Build, models: true do let(:pipeline) do create(:ci_pipeline, project: project, - sha: project.commit.id) + sha: project.commit.id, + ref: 'fix') end let(:build) { create(:ci_build, pipeline: pipeline) } @@ -698,7 +699,7 @@ describe Ci::Build, models: true do end it 'returns builds from ref' do - builds = project.latest_success_builds_for('HEAD') + builds = project.latest_success_builds_for('fix') expect(builds).to contain_exactly(build) end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index bc6242a0d71..fb0f066498a 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -190,7 +190,7 @@ describe API::API, api: true do describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do include_context 'artifacts from ref and build name' - def path_from_ref(ref = pipeline.sha, job = build.name) + def path_from_ref(ref = pipeline.ref, job = build.name) api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", user) end diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index c8a21f7d0ec..1782d37008a 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -10,7 +10,7 @@ describe Projects::ArtifactsController do end def path_from_ref( - ref = pipeline.sha, job = build.name, path = 'browse') + ref = pipeline.ref, job = build.name, path = 'browse') search_namespace_project_artifacts_path( project.namespace, project, diff --git a/spec/requests/shared/artifacts_context.rb b/spec/requests/shared/artifacts_context.rb index 0c9f33bfcb2..ff74b72a0b3 100644 --- a/spec/requests/shared/artifacts_context.rb +++ b/spec/requests/shared/artifacts_context.rb @@ -2,7 +2,10 @@ shared_context 'artifacts from ref and build name' do let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit('fix').sha) + create(:ci_pipeline, + project: project, + sha: project.commit('fix').sha, + ref: 'fix') end let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } @@ -30,17 +33,10 @@ shared_examples 'artifacts from ref with 404' do end shared_examples 'artifacts from ref successfully' do - context 'with sha' do - before do - get path_from_ref - end - - it('gives the file') { verify } - end - context 'with regular branch' do before do - pipeline.update(sha: project.commit('master').sha) + pipeline.update(ref: 'master', + sha: project.commit('master').sha) end before do @@ -52,7 +48,8 @@ shared_examples 'artifacts from ref successfully' do context 'with branch name containing slash' do before do - pipeline.update(sha: project.commit('improve/awesome').sha) + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) end before do From af86b8c2c2b6fb08ea55eb89f1dd20aba81862ae Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 21:11:53 +0800 Subject: [PATCH 053/141] Latest success pipelines (rather than builds) --- app/models/ci/pipeline.rb | 8 ++++++ app/models/commit_status.rb | 4 +-- app/models/project.rb | 7 ++--- spec/models/build_spec.rb | 35 +++++++++++++++++------ spec/requests/shared/artifacts_context.rb | 19 +++++++----- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fa4071e2482..431a91004cd 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -18,6 +18,14 @@ module Ci after_touch :update_state after_save :keep_around_commits + scope :latest, -> do + max_id = unscope(:select). + select("max(#{table_name}.id)"). + group(:ref) + + where(id: max_id) + end + def self.truncate_sha(sha) sha[0...8] end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index baabbd785cc..3e97fe68d4b 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -18,8 +18,8 @@ class CommitStatus < ActiveRecord::Base scope :latest, -> do max_id = unscope(:select). - select("max(#{table_name}.id)"). - group(:name, :commit_id) + select("max(#{table_name}.id)"). + group(:name, :commit_id) where(id: max_id) end diff --git a/app/models/project.rb b/app/models/project.rb index 770ec1c8a68..1578fe67581 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -430,12 +430,9 @@ class Project < ActiveRecord::Base end def latest_success_builds_for(ref = 'HEAD') - builds_for(ref).success.latest - end - - def builds_for(ref = 'HEAD') Ci::Build.joins(:pipeline). - merge(Ci::Pipeline.where(ref: ref, project: self)) + merge(pipelines.where(ref: ref).success.latest). + with_artifacts end def merge_base_commit(first_commit_id, second_commit_id) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 53064138a50..8a8a4b46f08 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -6,7 +6,8 @@ describe Ci::Build, models: true do let(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id, - ref: 'fix') + ref: 'fix', + status: 'success') end let(:build) { create(:ci_build, pipeline: pipeline) } @@ -694,20 +695,38 @@ describe Ci::Build, models: true do end describe 'Project#latest_success_builds_for' do + let(:build) do + create(:ci_build, :artifacts, :success, pipeline: pipeline) + end + before do - build.update(status: 'success') + build end - it 'returns builds from ref' do - builds = project.latest_success_builds_for('fix') + context 'with succeed pipeline' do + it 'returns builds from ref' do + builds = project.latest_success_builds_for('fix') - expect(builds).to contain_exactly(build) + expect(builds).to contain_exactly(build) + end + + it 'returns empty relation if the build cannot be found' do + builds = project.latest_success_builds_for('TAIL').all + + expect(builds).to be_empty + end end - it 'returns empty relation if the build cannot be found' do - builds = project.latest_success_builds_for('TAIL').all + context 'with pending pipeline' do + before do + pipeline.update(status: 'pending') + end - expect(builds).to be_empty + it 'returns empty relation' do + builds = project.latest_success_builds_for('fix').all + + expect(builds).to be_empty + end end end end diff --git a/spec/requests/shared/artifacts_context.rb b/spec/requests/shared/artifacts_context.rb index ff74b72a0b3..635c5646f91 100644 --- a/spec/requests/shared/artifacts_context.rb +++ b/spec/requests/shared/artifacts_context.rb @@ -25,7 +25,7 @@ shared_examples 'artifacts from ref with 404' do context 'has no such build' do before do - get path_from_ref(pipeline.sha, 'NOBUILD') + get path_from_ref(pipeline.ref, 'NOBUILD') end it('gives 404') { verify } @@ -33,6 +33,11 @@ shared_examples 'artifacts from ref with 404' do end shared_examples 'artifacts from ref successfully' do + def create_new_pipeline(status) + new_pipeline = create(:ci_pipeline, status: 'success') + create(:ci_build, status, :artifacts, pipeline: new_pipeline) + end + context 'with regular branch' do before do pipeline.update(ref: 'master', @@ -59,10 +64,10 @@ shared_examples 'artifacts from ref successfully' do it('gives the file') { verify } end - context 'with latest build' do + context 'with latest pipeline' do before do - 3.times do # creating some old builds - create(:ci_build, :success, :artifacts, pipeline: pipeline) + 3.times do # creating some old pipelines + create_new_pipeline(:success) end end @@ -73,10 +78,10 @@ shared_examples 'artifacts from ref successfully' do it('gives the file') { verify } end - context 'with success build' do + context 'with success pipeline' do before do - build # make sure build was old, but still the latest success one - create(:ci_build, :pending, :artifacts, pipeline: pipeline) + build # make sure pipeline was old, but still the latest success one + create_new_pipeline(:pending) end before do From e51d4a05b7195a98b48548c4c7260886f956b6d2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 21:17:16 +0800 Subject: [PATCH 054/141] We should actually give latest success builds as well --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 1578fe67581..d26aa8073e8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -432,7 +432,7 @@ class Project < ActiveRecord::Base def latest_success_builds_for(ref = 'HEAD') Ci::Build.joins(:pipeline). merge(pipelines.where(ref: ref).success.latest). - with_artifacts + with_artifacts.success.latest end def merge_base_commit(first_commit_id, second_commit_id) From dcb436f58f16b20094d126bf0eb9e3403905c7cc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 22:07:26 +0800 Subject: [PATCH 055/141] Use Project#latest_success_builds_for --- app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 78b3de46f58..ced0380bb0b 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,7 +27,7 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - - artifacts = @project.builds_for(@repository.root_ref).latest.with_artifacts + - artifacts = @project.latest_success_builds_for(@repository.root_ref) - if artifacts.any? .dropdown.inline.artifacts-btn %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index f504d514963..2eedc0d9a42 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -14,7 +14,7 @@ %li = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - artifacts = @project.builds_for(@ref).latest.with_artifacts + - artifacts = @project.latest_success_builds_for(@ref) - if artifacts.any? %li.dropdown-header Artifacts - artifacts.each do |job| diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 1f4c6c9ec08..4a8ef77ad8b 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -12,7 +12,7 @@ %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - artifacts = project.builds_for(ref).latest.with_artifacts + - artifacts = project.latest_success_builds_for(ref) - if artifacts.any? %li.dropdown-header Artifacts - artifacts.each do |job| From 72699d9719a269a82be6af0812d86d84733c77bd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 23:16:29 +0800 Subject: [PATCH 056/141] Should be branch.name, not root ref --- app/views/projects/branches/_branch.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index ced0380bb0b..c2d7237f142 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,7 +27,7 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - - artifacts = @project.latest_success_builds_for(@repository.root_ref) + - artifacts = @project.latest_success_builds_for(branch.name) - if artifacts.any? .dropdown.inline.artifacts-btn %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } From 57a78c37c72b2d697bc863ebfb84d3ca61ba9d7b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 23:17:43 +0800 Subject: [PATCH 057/141] Show notice if builds are not from latest pipeline --- app/models/ci/build.rb | 3 ++ app/models/project.rb | 12 +++++-- app/views/projects/branches/_branch.html.haml | 32 +++++++++++-------- .../projects/buttons/_download.html.haml | 18 +++++++---- app/views/projects/tags/_download.html.haml | 18 +++++++---- 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ffac3a22efc..9af04964b85 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -15,6 +15,9 @@ module Ci scope :with_artifacts, ->() { where.not(artifacts_file: nil) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } + scope :latest_success_with_artifacts, ->() do + with_artifacts.success.latest + end mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader diff --git a/app/models/project.rb b/app/models/project.rb index 12851c5d0ec..77431c3f538 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -429,10 +429,16 @@ class Project < ActiveRecord::Base repository.commit(ref) end - def latest_success_builds_for(ref = 'HEAD') + # ref can't be HEAD or SHA, can only be branch/tag name + def latest_success_pipeline_for(ref = 'master') + pipelines.where(ref: ref).success.latest + end + + # ref can't be HEAD or SHA, can only be branch/tag name + def latest_success_builds_for(ref = 'master') Ci::Build.joins(:pipeline). - merge(pipelines.where(ref: ref).success.latest). - with_artifacts.success.latest + merge(latest_success_pipeline_for(ref)). + latest_success_with_artifacts end def merge_base_commit(first_commit_id, second_commit_id) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index c2d7237f142..084a8474c4f 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,20 +27,24 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - - artifacts = @project.latest_success_builds_for(branch.name) - - if artifacts.any? - .dropdown.inline.artifacts-btn - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - = icon('download') - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Artifacts - - artifacts.each do |job| - %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do - %span Download '#{job.name}' + - pipeline = @project.latest_success_pipeline_for(branch.name).first + - if pipeline + - artifacts = pipeline.builds.latest_success_with_artifacts + - if artifacts.any? + .dropdown.inline.artifacts-btn + %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + = icon('download') + %span.caret + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Artifacts + - unless pipeline.latest? + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(@project.namespace, @project, pipeline.sha))})" + - artifacts.each do |job| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do + %span Download '#{job.name}' - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 2eedc0d9a42..24a11005e61 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -14,10 +14,14 @@ %li = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - artifacts = @project.latest_success_builds_for(@ref) - - if artifacts.any? - %li.dropdown-header Artifacts - - artifacts.each do |job| - %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do - %span Download '#{job.name}' + - pipeline = @project.latest_success_pipeline_for(@ref).first + - if pipeline + - artifacts = pipeline.builds.latest_success_with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(@project.namespace, @project, pipeline.sha))})" + - artifacts.each do |job| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do + %span Download '#{job.name}' diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 4a8ef77ad8b..d2650809a3e 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -12,10 +12,14 @@ %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - artifacts = project.latest_success_builds_for(ref) - - if artifacts.any? - %li.dropdown-header Artifacts - - artifacts.each do |job| - %li - = link_to download_namespace_project_build_artifacts_path(project.namespace, project, job), rel: 'nofollow' do - %span Download '#{job.name}' + - pipeline = project.latest_success_pipeline_for(ref).first + - if pipeline + - artifacts = pipeline.builds.latest_success_with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(project.namespace, project, pipeline.sha))})" + - artifacts.each do |job| + %li + = link_to download_namespace_project_build_artifacts_path(project.namespace, project, job), rel: 'nofollow' do + %span Download '#{job.name}' From 914336b2d9da688a7b6267425a408c0ce280f9b2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 23:26:30 +0800 Subject: [PATCH 058/141] Links to search_namespace_project_artifacts_path instead --- app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 084a8474c4f..bb1f2ec9604 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -43,7 +43,7 @@ = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(@project.namespace, @project, pipeline.sha))})" - artifacts.each do |job| %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do + = link_to search_namespace_project_artifacts_path(@project.namespace, @project, branch.name, 'download', job: job.name), rel: 'nofollow' do %span Download '#{job.name}' - if can_remove_branch?(@project, branch.name) diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 24a11005e61..558a9023ed2 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -23,5 +23,5 @@ = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(@project.namespace, @project, pipeline.sha))})" - artifacts.each do |job| %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do + = link_to search_namespace_project_artifacts_path(@project.namespace, @project, @ref, 'download', job: job.name), rel: 'nofollow' do %span Download '#{job.name}' diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index d2650809a3e..2a8891e917f 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -21,5 +21,5 @@ = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(project.namespace, project, pipeline.sha))})" - artifacts.each do |job| %li - = link_to download_namespace_project_build_artifacts_path(project.namespace, project, job), rel: 'nofollow' do + = link_to search_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do %span Download '#{job.name}' From e00fb385b247f90f12ddadd090e87cd59f2ebb36 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 18 Jul 2016 23:34:31 +0800 Subject: [PATCH 059/141] Actually should use tree path --- app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index bb1f2ec9604..cbd6ab74128 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -40,7 +40,7 @@ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(@project.namespace, @project, pipeline.sha))})" + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(@project.namespace, @project, pipeline.sha))})" - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, branch.name, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 558a9023ed2..f96045e09f0 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -20,7 +20,7 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(@project.namespace, @project, pipeline.sha))})" + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(@project.namespace, @project, pipeline.sha))})" - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, @ref, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 2a8891e917f..6a431bcf7b2 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -18,7 +18,7 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_commits_path(project.namespace, project, pipeline.sha))})" + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(project.namespace, project, pipeline.sha))})" - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do From 5fa6af05eb42feed7e0ca69778019805f7780ea5 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 18 Jul 2016 21:02:18 -0700 Subject: [PATCH 060/141] Add artifacts to view branch page download dropdown --- .../repositories/_download_archive.html.haml | 29 +++++++++---------- app/views/projects/tree/show.html.haml | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 24658319060..5b2ddce3e91 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -1,16 +1,14 @@ - ref = ref || nil - btn_class = btn_class || '' -- split_button = split_button || false -- if split_button == true - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } +%span.btn-group{class: btn_class} + .dropdown.inline + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') %span.caret %span.sr-only Select Archive Format - %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code %li = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do %i.fa.fa-download @@ -27,11 +25,10 @@ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.fa.fa-download %span Download tar -- else - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span zip - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span tar.gz + - artifacts = @project.latest_success_builds_for(@ref) + - if artifacts.any? + %li.dropdown-header Artifacts + - artifacts.each do |job| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do + %span Download '#{job.name}' diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index bf5360b4dee..c68f86f1378 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -11,7 +11,7 @@ .tree-controls = render 'projects/find_file_link' - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped' #tree-holder.tree-holder.clearfix .nav-block From 1a41cb90175ac8d6f780bf4fd35e4862f5312574 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 19 Jul 2016 16:50:53 +0800 Subject: [PATCH 061/141] Fix links and add not latest notice --- .../repositories/_download_archive.html.haml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 5b2ddce3e91..396fb8598ae 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -25,10 +25,14 @@ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.fa.fa-download %span Download tar - - artifacts = @project.latest_success_builds_for(@ref) - - if artifacts.any? - %li.dropdown-header Artifacts - - artifacts.each do |job| - %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, job), rel: 'nofollow' do - %span Download '#{job.name}' + - pipeline = @project.latest_success_pipeline_for(ref).first + - if pipeline + - artifacts = pipeline.builds.latest_success_with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(@project.namespace, @project, pipeline.sha))})" + - artifacts.each do |job| + %li + = link_to search_namespace_project_artifacts_path(@project.namespace, @project, ref, 'download', job: job.name), rel: 'nofollow' do + %span Download '#{job.name}' From fba2ec45b3bf493611f2d7e7e13a21c39bc654e0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 19 Jul 2016 17:08:21 +0800 Subject: [PATCH 062/141] Just use order(id: :desc) for latest stuffs: We don't need that subquery for group by ref and alike here. --- app/models/ci/build.rb | 2 +- app/models/ci/pipeline.rb | 10 +--------- app/models/project.rb | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9af04964b85..c048eff0f80 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -16,7 +16,7 @@ module Ci scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :latest_success_with_artifacts, ->() do - with_artifacts.success.latest + with_artifacts.success.order(id: :desc) end mount_uploader :artifacts_file, ArtifactUploader diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index a8e6a23e1c4..148b056789a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -20,14 +20,6 @@ module Ci after_touch :update_state after_save :keep_around_commits - scope :latest, -> do - max_id = unscope(:select). - select("max(#{table_name}.id)"). - group(:ref) - - where(id: max_id) - end - def self.truncate_sha(sha) sha[0...8] end @@ -226,7 +218,7 @@ module Ci def keep_around_commits return unless project - + project.repository.keep_around(self.sha) project.repository.keep_around(self.before_sha) end diff --git a/app/models/project.rb b/app/models/project.rb index 77431c3f538..30e8ade99ff 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -431,7 +431,7 @@ class Project < ActiveRecord::Base # ref can't be HEAD or SHA, can only be branch/tag name def latest_success_pipeline_for(ref = 'master') - pipelines.where(ref: ref).success.latest + pipelines.where(ref: ref).success.order(id: :desc) end # ref can't be HEAD or SHA, can only be branch/tag name From 0538e1e934484e76575164314fe8451374e4a4c8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 19 Jul 2016 17:09:32 +0800 Subject: [PATCH 063/141] Support SHA for downloading artifacts: So if we also query against SHA, we could actually support SHA. If there's a branch or tag also named like SHA this could be ambiguous, but since we could already do that in Git, I think it's probably fine, people would be aware they shouldn't use the same name anyway. --- app/models/project.rb | 9 ++++++--- spec/requests/shared/artifacts_context.rb | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 30e8ade99ff..c1cb1558132 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -429,12 +429,15 @@ class Project < ActiveRecord::Base repository.commit(ref) end - # ref can't be HEAD or SHA, can only be branch/tag name + # ref can't be HEAD, can only be branch/tag name or SHA def latest_success_pipeline_for(ref = 'master') - pipelines.where(ref: ref).success.order(id: :desc) + table = Ci::Pipeline.quoted_table_name + # TODO: Use `where(ref: ref).or(sha: ref)` in Rails 5 + pipelines.where("#{table}.ref = ? OR #{table}.sha = ?", ref, ref). + success.order(id: :desc) end - # ref can't be HEAD or SHA, can only be branch/tag name + # ref can't be HEAD, can only be branch/tag name or SHA def latest_success_builds_for(ref = 'master') Ci::Build.joins(:pipeline). merge(latest_success_pipeline_for(ref)). diff --git a/spec/requests/shared/artifacts_context.rb b/spec/requests/shared/artifacts_context.rb index 635c5646f91..102ae392844 100644 --- a/spec/requests/shared/artifacts_context.rb +++ b/spec/requests/shared/artifacts_context.rb @@ -38,6 +38,14 @@ shared_examples 'artifacts from ref successfully' do create(:ci_build, status, :artifacts, pipeline: new_pipeline) end + context 'with sha' do + before do + get path_from_ref(pipeline.sha) + end + + it('gives the file') { verify } + end + context 'with regular branch' do before do pipeline.update(ref: 'master', From 85ceb8b72f5a67d21bc9530fe835fdece98f3d4e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 19 Jul 2016 17:51:45 +0800 Subject: [PATCH 064/141] Rename latest_success* to latest_successful: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13164642 --- app/controllers/projects/artifacts_controller.rb | 2 +- app/models/ci/build.rb | 2 +- app/models/project.rb | 8 ++++---- app/views/projects/branches/_branch.html.haml | 4 ++-- app/views/projects/buttons/_download.html.haml | 4 ++-- .../projects/repositories/_download_archive.html.haml | 4 ++-- app/views/projects/tags/_download.html.haml | 4 ++-- lib/api/builds.rb | 2 +- spec/models/build_spec.rb | 8 ++++---- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f33cf238d88..05112571225 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -60,7 +60,7 @@ class Projects::ArtifactsController < Projects::ApplicationController def build_from_ref if params[:ref_name] - builds = project.latest_success_builds_for(params[:ref_name]) + builds = project.latest_successful_builds_for(params[:ref_name]) builds.find_by(name: params[:job]) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index c048eff0f80..65dfe4f0190 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -15,7 +15,7 @@ module Ci scope :with_artifacts, ->() { where.not(artifacts_file: nil) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } - scope :latest_success_with_artifacts, ->() do + scope :latest_successful_with_artifacts, ->() do with_artifacts.success.order(id: :desc) end diff --git a/app/models/project.rb b/app/models/project.rb index c1cb1558132..60928bf9922 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -430,7 +430,7 @@ class Project < ActiveRecord::Base end # ref can't be HEAD, can only be branch/tag name or SHA - def latest_success_pipeline_for(ref = 'master') + def latest_successful_pipeline_for(ref = 'master') table = Ci::Pipeline.quoted_table_name # TODO: Use `where(ref: ref).or(sha: ref)` in Rails 5 pipelines.where("#{table}.ref = ? OR #{table}.sha = ?", ref, ref). @@ -438,10 +438,10 @@ class Project < ActiveRecord::Base end # ref can't be HEAD, can only be branch/tag name or SHA - def latest_success_builds_for(ref = 'master') + def latest_successful_builds_for(ref = 'master') Ci::Build.joins(:pipeline). - merge(latest_success_pipeline_for(ref)). - latest_success_with_artifacts + merge(latest_successful_pipeline_for(ref)). + latest_successful_with_artifacts end def merge_base_commit(first_commit_id, second_commit_id) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index cbd6ab74128..8f6ddfd9044 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,9 +27,9 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - - pipeline = @project.latest_success_pipeline_for(branch.name).first + - pipeline = @project.latest_successful_pipeline_for(branch.name).first - if pipeline - - artifacts = pipeline.builds.latest_success_with_artifacts + - artifacts = pipeline.builds.latest_successful_with_artifacts - if artifacts.any? .dropdown.inline.artifacts-btn %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index f96045e09f0..047931a7fa5 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -14,9 +14,9 @@ %li = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - pipeline = @project.latest_success_pipeline_for(@ref).first + - pipeline = @project.latest_successful_pipeline_for(@ref).first - if pipeline - - artifacts = pipeline.builds.latest_success_with_artifacts + - artifacts = pipeline.builds.latest_successful_with_artifacts - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 396fb8598ae..1c03aa0a332 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -25,9 +25,9 @@ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.fa.fa-download %span Download tar - - pipeline = @project.latest_success_pipeline_for(ref).first + - pipeline = @project.latest_successful_pipeline_for(ref).first - if pipeline - - artifacts = pipeline.builds.latest_success_with_artifacts + - artifacts = pipeline.builds.latest_successful_with_artifacts - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 6a431bcf7b2..8e5e5cb559b 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -12,9 +12,9 @@ %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - pipeline = project.latest_success_pipeline_for(ref).first + - pipeline = project.latest_successful_pipeline_for(ref).first - if pipeline - - artifacts = pipeline.builds.latest_success_with_artifacts + - artifacts = pipeline.builds.latest_successful_with_artifacts - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? diff --git a/lib/api/builds.rb b/lib/api/builds.rb index a27397a82f7..bb9e8f1ae6e 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -83,7 +83,7 @@ module API # GET /projects/:id/artifacts/:ref_name/download?job=name get ':id/builds/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do - builds = user_project.latest_success_builds_for(params[:ref_name]) + builds = user_project.latest_successful_builds_for(params[:ref_name]) latest_build = builds.find_by!(name: params[:job]) present_artifact!(latest_build.artifacts_file) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index d435cd745b3..355cb8fdfff 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -678,7 +678,7 @@ describe Ci::Build, models: true do end end - describe 'Project#latest_success_builds_for' do + describe 'Project#latest_successful_builds_for' do let(:build) do create(:ci_build, :artifacts, :success, pipeline: pipeline) end @@ -689,13 +689,13 @@ describe Ci::Build, models: true do context 'with succeed pipeline' do it 'returns builds from ref' do - builds = project.latest_success_builds_for('fix') + builds = project.latest_successful_builds_for('fix') expect(builds).to contain_exactly(build) end it 'returns empty relation if the build cannot be found' do - builds = project.latest_success_builds_for('TAIL').all + builds = project.latest_successful_builds_for('TAIL').all expect(builds).to be_empty end @@ -707,7 +707,7 @@ describe Ci::Build, models: true do end it 'returns empty relation' do - builds = project.latest_success_builds_for('fix').all + builds = project.latest_successful_builds_for('fix').all expect(builds).to be_empty end From e3ce02300bf90451b98479720d1093afe8b7eea8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 19 Jul 2016 18:03:21 +0800 Subject: [PATCH 065/141] Link to pipeline instead of source tree, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13164795 --- app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- app/views/projects/repositories/_download_archive.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 8f6ddfd9044..e48b78f9eef 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -40,7 +40,7 @@ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(@project.namespace, @project, pipeline.sha))})" + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(@project.namespace, @project, pipeline))})" - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, branch.name, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 047931a7fa5..32f911f6b31 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -20,7 +20,7 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(@project.namespace, @project, pipeline.sha))})" + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(@project.namespace, @project, pipeline))})" - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, @ref, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 1c03aa0a332..e29d99371c8 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -31,7 +31,7 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(@project.namespace, @project, pipeline.sha))})" + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(@project.namespace, @project, pipeline))})" - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, ref, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 8e5e5cb559b..8acf145049a 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -18,7 +18,7 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_tree_path(project.namespace, project, pipeline.sha))})" + = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(project.namespace, project, pipeline))})" - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do From 69d0671342f361f48001c1c9bb4571cd881fdca8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 21 Jul 2016 18:47:05 +0800 Subject: [PATCH 066/141] Restore to what it used to be --- spec/requests/api/builds_spec.rb | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 56eb9cd8f8d..0d9820df18f 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative '../shared/artifacts_context' describe API::API, api: true do include ApiHelpers @@ -17,9 +16,6 @@ describe API::API, api: true do let(:query) { '' } before do - developer - build - get api("/projects/#{project.id}/builds?#{query}", api_user) end @@ -83,9 +79,9 @@ describe API::API, api: true do context 'when user is authorized' do context 'when pipeline has builds' do before do - developer - build + create(:ci_pipeline, project: project, sha: project.commit.id) create(:ci_build, pipeline: pipeline) + create(:ci_build) get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) end @@ -99,8 +95,6 @@ describe API::API, api: true do context 'when pipeline has no builds' do before do - developer - branch_head = project.commit('feature').id get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) end @@ -115,7 +109,8 @@ describe API::API, api: true do context 'when user is not authorized' do before do - build + create(:ci_pipeline, project: project, sha: project.commit.id) + create(:ci_build, pipeline: pipeline) get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) end @@ -130,8 +125,6 @@ describe API::API, api: true do describe 'GET /projects/:id/builds/:build_id' do before do - developer - get api("/projects/#{project.id}/builds/#{build.id}", api_user) end @@ -153,8 +146,6 @@ describe API::API, api: true do describe 'GET /projects/:id/builds/:build_id/artifacts' do before do - developer - get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) end @@ -304,9 +295,6 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/cancel' do before do - developer - reporter - post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) end @@ -340,9 +328,6 @@ describe API::API, api: true do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } before do - developer - reporter - post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) end @@ -375,8 +360,6 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/erase' do before do - developer - post api("/projects/#{project.id}/builds/#{build.id}/erase", user) end @@ -407,8 +390,6 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do before do - developer - post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) end From fa02d8fcc588b3720640131e7cc9ef7b06470932 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 21 Jul 2016 19:09:46 +0800 Subject: [PATCH 067/141] Merge shared context into controller test and update accordingly --- .../projects/artifacts_controller_spec.rb | 77 ++++++++++--- spec/requests/shared/artifacts_context.rb | 101 ------------------ 2 files changed, 62 insertions(+), 116 deletions(-) delete mode 100644 spec/requests/shared/artifacts_context.rb diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 1782d37008a..b823676d9e1 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -1,9 +1,20 @@ require 'spec_helper' -require_relative '../shared/artifacts_context' describe Projects::ArtifactsController do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch) + end + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do - include_context 'artifacts from ref and build name' + before do + project.team << [user, :developer] + end before do login_as(user) @@ -19,33 +30,69 @@ describe Projects::ArtifactsController do job: job) end - context '404' do - def verify - expect(response.status).to eq(404) + context 'cannot find the build' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } end - it_behaves_like 'artifacts from ref with 404' + context 'has no such ref' do + before do + get path_from_ref('TAIL', build.name) + end + + it_behaves_like 'not found' + end + + context 'has no such build' do + before do + get path_from_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end context 'has no path' do before do get path_from_ref(pipeline.sha, build.name, '') end - it('gives 404') { verify } + it_behaves_like 'not found' end end - context '302' do - def verify - path = browse_namespace_project_build_artifacts_path( - project.namespace, - project, - build) + context 'found the build and redirect' do + shared_examples 'redirect to the build' do + it 'redirects' do + path = browse_namespace_project_build_artifacts_path( + project.namespace, + project, + build) - expect(response).to redirect_to(path) + expect(response).to redirect_to(path) + end end - it_behaves_like 'artifacts from ref successfully' + context 'with regular branch' do + before do + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get path_from_ref('master') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name containing slash' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome') + end + + it_behaves_like 'redirect to the build' + end end end end diff --git a/spec/requests/shared/artifacts_context.rb b/spec/requests/shared/artifacts_context.rb deleted file mode 100644 index 102ae392844..00000000000 --- a/spec/requests/shared/artifacts_context.rb +++ /dev/null @@ -1,101 +0,0 @@ -shared_context 'artifacts from ref and build name' do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:pipeline) do - create(:ci_pipeline, - project: project, - sha: project.commit('fix').sha, - ref: 'fix') - end - let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - - before do - project.team << [user, :developer] - end -end - -shared_examples 'artifacts from ref with 404' do - context 'has no such ref' do - before do - get path_from_ref('TAIL', build.name) - end - - it('gives 404') { verify } - end - - context 'has no such build' do - before do - get path_from_ref(pipeline.ref, 'NOBUILD') - end - - it('gives 404') { verify } - end -end - -shared_examples 'artifacts from ref successfully' do - def create_new_pipeline(status) - new_pipeline = create(:ci_pipeline, status: 'success') - create(:ci_build, status, :artifacts, pipeline: new_pipeline) - end - - context 'with sha' do - before do - get path_from_ref(pipeline.sha) - end - - it('gives the file') { verify } - end - - context 'with regular branch' do - before do - pipeline.update(ref: 'master', - sha: project.commit('master').sha) - end - - before do - get path_from_ref('master') - end - - it('gives the file') { verify } - end - - context 'with branch name containing slash' do - before do - pipeline.update(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) - end - - before do - get path_from_ref('improve/awesome') - end - - it('gives the file') { verify } - end - - context 'with latest pipeline' do - before do - 3.times do # creating some old pipelines - create_new_pipeline(:success) - end - end - - before do - get path_from_ref - end - - it('gives the file') { verify } - end - - context 'with success pipeline' do - before do - build # make sure pipeline was old, but still the latest success one - create_new_pipeline(:pending) - end - - before do - get path_from_ref - end - - it('gives the file') { verify } - end -end From 2d9e7468de7edfe1868b8d9dc6dcdaff116f0da8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 21 Jul 2016 20:00:33 +0800 Subject: [PATCH 068/141] They were moved to project_spec.rb --- spec/models/build_spec.rb | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 950580fdee3..13ef60b732a 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -915,40 +915,4 @@ describe Ci::Build, models: true do end end end - - describe 'Project#latest_successful_builds_for' do - let(:build) do - create(:ci_build, :artifacts, :success, pipeline: pipeline) - end - - before do - build - end - - context 'with succeed pipeline' do - it 'returns builds from ref' do - builds = project.latest_successful_builds_for('fix') - - expect(builds).to contain_exactly(build) - end - - it 'returns empty relation if the build cannot be found' do - builds = project.latest_successful_builds_for('TAIL').all - - expect(builds).to be_empty - end - end - - context 'with pending pipeline' do - before do - pipeline.update(status: 'pending') - end - - it 'returns empty relation' do - builds = project.latest_successful_builds_for('fix').all - - expect(builds).to be_empty - end - end - end end From 80c22e4c09d7808d2a971c78a6232d4843c8c4f7 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 25 Jul 2016 20:52:18 +0800 Subject: [PATCH 069/141] Add four features tests for download buttons in different places --- .../branches/download_buttons_spec.rb | 37 ++++++++++++++++++ .../projects/files/download_buttons_spec.rb | 38 +++++++++++++++++++ .../projects/main/download_buttons_spec.rb | 37 ++++++++++++++++++ .../projects/tags/download_buttons_spec.rb | 38 +++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 spec/features/projects/branches/download_buttons_spec.rb create mode 100644 spec/features/projects/files/download_buttons_spec.rb create mode 100644 spec/features/projects/main/download_buttons_spec.rb create mode 100644 spec/features/projects/tags/download_buttons_spec.rb diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb new file mode 100644 index 00000000000..d3f53b65699 --- /dev/null +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +feature 'Download buttons in branches page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + given(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking branches' do + context 'with artifacts' do + before do + visit namespace_project_branches_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb new file mode 100644 index 00000000000..60c2ffedce0 --- /dev/null +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +feature 'Download buttons in files tree', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + given(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when files tree' do + context 'with artifacts' do + before do + visit namespace_project_tree_path( + project.namespace, project, project.default_branch) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb new file mode 100644 index 00000000000..62e56808558 --- /dev/null +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +feature 'Download buttons in project main page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + given(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking project main page' do + context 'with artifacts' do + before do + visit namespace_project_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb new file mode 100644 index 00000000000..d4c4cfe9c99 --- /dev/null +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +feature 'Download buttons in tags page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:tag) { 'v1.0.0' } + given(:project) { create(:project) } + given(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: tag, + status: status) + end + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking tags' do + context 'with artifacts' do + before do + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end From 3eae0641ef0708f9b223abbe0070e332ea0b20ac Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 10 Aug 2016 18:40:10 +0800 Subject: [PATCH 070/141] Introduce Pipeline#latest and Pipeline.latest_for: So that we could easily access it for the view --- app/models/ci/pipeline.rb | 12 +++++++-- spec/models/ci/pipeline_spec.rb | 48 +++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bce6a992af6..bc1190537da 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,8 +21,12 @@ module Ci after_save :keep_around_commits # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest_successful_for, ->(ref = default_branch) do - where(ref: ref).success.order(id: :desc).limit(1) + scope :latest_successful_for, ->(ref) do + latest(ref).success + end + + scope :latest_for, ->(ref) do + where(ref: ref).order(id: :desc).limit(1) end def self.truncate_sha(sha) @@ -98,6 +102,10 @@ module Ci end end + def latest + project.pipelines.latest_for(ref).first + end + def latest? return false unless ref commit = project.commit(ref) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ccee591cf7a..556a6e1b59a 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe Ci::Pipeline, models: true do - let(:project) { FactoryGirl.create :empty_project } - let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:user) } @@ -481,6 +481,50 @@ describe Ci::Pipeline, models: true do end end + context 'with non-empty project' do + let(:project) { create(:project) } + let(:pipeline) { create_pipeline } + + describe '#latest?' do + context 'with latest sha' do + it 'returns true' do + expect(pipeline).to be_latest + end + end + + context 'with not latest sha' do + before do + pipeline.update( + sha: project.commit("#{project.default_branch}~1").sha) + end + + it 'returns false' do + expect(pipeline).not_to be_latest + end + end + end + + describe '#latest' do + let(:previous_pipeline) { create_pipeline } + + before do + previous_pipeline + pipeline + end + + it 'gives the latest pipeline' do + expect(previous_pipeline.latest).to eq(pipeline) + end + end + + def create_pipeline + create(:ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha) + end + end + describe '#manual_actions' do subject { pipeline.manual_actions } From cc3dbf83f4c0cf21fee56398f27851981f0e98f6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 10 Aug 2016 18:57:51 +0800 Subject: [PATCH 071/141] Empty lines around blocks --- .../projects/branches/download_buttons_spec.rb | 11 +++++++---- spec/features/projects/files/download_buttons_spec.rb | 11 +++++++---- spec/features/projects/main/download_buttons_spec.rb | 11 +++++++---- spec/features/projects/tags/download_buttons_spec.rb | 11 +++++++---- spec/requests/projects/artifacts_controller_spec.rb | 2 ++ 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index d3f53b65699..a223786777b 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -5,12 +5,15 @@ feature 'Download buttons in branches page', feature: true do given(:role) { :developer } given(:status) { 'success' } given(:project) { create(:project) } + given(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.sha, - ref: project.default_branch, - status: status) + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) end + given!(:build) do create(:ci_build, :success, :artifacts, pipeline: pipeline, diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb index 60c2ffedce0..be5cebcd7c9 100644 --- a/spec/features/projects/files/download_buttons_spec.rb +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -5,12 +5,15 @@ feature 'Download buttons in files tree', feature: true do given(:role) { :developer } given(:status) { 'success' } given(:project) { create(:project) } + given(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.sha, - ref: project.default_branch, - status: status) + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) end + given!(:build) do create(:ci_build, :success, :artifacts, pipeline: pipeline, diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb index 62e56808558..b26c0ea7a14 100644 --- a/spec/features/projects/main/download_buttons_spec.rb +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -5,12 +5,15 @@ feature 'Download buttons in project main page', feature: true do given(:role) { :developer } given(:status) { 'success' } given(:project) { create(:project) } + given(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.sha, - ref: project.default_branch, - status: status) + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) end + given!(:build) do create(:ci_build, :success, :artifacts, pipeline: pipeline, diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb index d4c4cfe9c99..ebc5204cf1d 100644 --- a/spec/features/projects/tags/download_buttons_spec.rb +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -6,12 +6,15 @@ feature 'Download buttons in tags page', feature: true do given(:status) { 'success' } given(:tag) { 'v1.0.0' } given(:project) { create(:project) } + given(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.sha, - ref: tag, - status: status) + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: tag, + status: status) end + given!(:build) do create(:ci_build, :success, :artifacts, pipeline: pipeline, diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index b823676d9e1..952b9fb99b7 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -3,12 +3,14 @@ require 'spec_helper' describe Projects::ArtifactsController do let(:user) { create(:user) } let(:project) { create(:project) } + let(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) end + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do From 729ad5d53f59b18f14f9c0c7ed306b34ae70c4b5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 10 Aug 2016 18:59:06 +0800 Subject: [PATCH 072/141] This might be fixed on master already, but well --- spec/lib/gitlab/redis_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index 879ed30841c..e54f5ffb312 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -67,7 +67,6 @@ describe Gitlab::Redis do expect(subject).to receive(:fetch_config) { 'redis://myredis:6379' } expect(subject.send(:raw_config_hash)).to eq(url: 'redis://myredis:6379') end - end describe '#fetch_config' do From 517249858e41694f51b67461b313d5a34c2a466c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 10 Aug 2016 21:07:26 +0800 Subject: [PATCH 073/141] It's latest_for, not just latest --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bc1190537da..50f9ee7fc66 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -22,7 +22,7 @@ module Ci # ref can't be HEAD or SHA, can only be branch/tag name scope :latest_successful_for, ->(ref) do - latest(ref).success + latest_for(ref).success end scope :latest_for, ->(ref) do From 4b559c9afb34b80b910efec514653c6ea65adba8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 11 Aug 2016 17:26:04 +0800 Subject: [PATCH 074/141] Reverse ref and sha in args and rename pipeline to pipeline_for --- app/models/merge_request.rb | 3 ++- app/models/project.rb | 7 ++++--- app/views/projects/issues/_related_branches.html.haml | 2 +- db/fixtures/development/14_builds.rb | 2 +- lib/api/commit_statuses.rb | 2 +- spec/models/project_spec.rb | 2 +- spec/requests/api/commits_spec.rb | 2 +- spec/services/ci/image_for_build_service_spec.rb | 2 +- 8 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b1fb3ce5d69..7c8e938df75 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -666,7 +666,8 @@ class MergeRequest < ActiveRecord::Base end def pipeline - @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project + return unless diff_head_sha && source_project + @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) end def merge_commit diff --git a/app/models/project.rb b/app/models/project.rb index d306f86f783..dc9b4b38a10 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1086,12 +1086,13 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def pipeline(sha, ref) + def pipeline_for(ref, sha) pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_pipeline(sha, ref, current_user = nil) - pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user) + def ensure_pipeline(ref, sha, current_user = nil) + pipeline_for(ref, sha) || + pipelines.create(sha: sha, ref: ref, user: current_user) end def enable_ci diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 6ea9f612d13..a8eeab3e55e 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,7 +5,7 @@ - @related_branches.each do |branch| %li - target = @project.repository.find_branch(branch).target - - pipeline = @project.pipeline(target.sha, branch) if target + - pipeline = @project.pipeline_for(branch, target.sha) if target - if pipeline %span.related-branch-ci-status = render_pipeline_status(pipeline) diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index e65abe4ef77..6cc18bf51ed 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -40,7 +40,7 @@ class Gitlab::Seeder::Builds commits = @project.repository.commits('master', limit: 5) commits_sha = commits.map { |commit| commit.raw.id } commits_sha.map do |sha| - @project.ensure_pipeline(sha, 'master') + @project.ensure_pipeline('master', sha) end rescue [] diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 4df6ca8333e..5e3c9563703 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -64,7 +64,7 @@ module API ref = branches.first end - pipeline = @project.ensure_pipeline(commit.sha, ref, current_user) + pipeline = @project.ensure_pipeline(ref, commit.sha, current_user) name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref]) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9c3b4712cab..60819fe02be 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -688,7 +688,7 @@ describe Project, models: true do let(:project) { create :project } let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } - subject { project.pipeline(pipeline.sha, 'master') } + subject { project.pipeline_for('master', pipeline.sha) } it { is_expected.to eq(pipeline) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 4379fcb3c1e..60c2f14bd3c 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -94,7 +94,7 @@ describe API::API, api: true do end it "returns status for CI" do - pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline('master', project.repository.commit.sha) get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response).to have_http_status(200) expect(json_response['status']).to eq(pipeline.status) diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 3a3e3efe709..21e00b11a12 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_pipeline(commit_sha, 'master') } + let(:commit) { project.ensure_pipeline('master', commit_sha) } let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } describe '#execute' do From 0a9d9f7d5096aa742564e704a96fa7c40eeaf007 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 11 Aug 2016 17:55:23 +0800 Subject: [PATCH 075/141] Fetch the current SHA if SHA was not passed --- app/models/project.rb | 3 ++- spec/models/project_spec.rb | 38 +++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index dc9b4b38a10..2969bec0bf7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1086,7 +1086,8 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def pipeline_for(ref, sha) + def pipeline_for(ref, sha = commit(ref).try(:sha)) + return unless sha pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 60819fe02be..1ce306c4f39 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -684,23 +684,37 @@ describe Project, models: true do end end - describe '#pipeline' do - let(:project) { create :project } - let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } + describe '#pipeline_for' do + let(:project) { create(:project) } + let!(:pipeline) { create_pipeline } - subject { project.pipeline_for('master', pipeline.sha) } + shared_examples 'giving the correct pipeline' do + it { is_expected.to eq(pipeline) } - it { is_expected.to eq(pipeline) } + context 'return latest' do + let!(:pipeline2) { create_pipeline } - context 'return latest' do - let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' } - - before do - pipeline - pipeline2 + it { is_expected.to eq(pipeline2) } end + end - it { is_expected.to eq(pipeline2) } + context 'with explicit sha' do + subject { project.pipeline_for('master', pipeline.sha) } + + it_behaves_like 'giving the correct pipeline' + end + + context 'with implicit sha' do + subject { project.pipeline_for('master') } + + it_behaves_like 'giving the correct pipeline' + end + + def create_pipeline + create(:ci_pipeline, + project: project, + ref: 'master', + sha: project.commit('master').sha) end end From 71046cc62919846b52d3724f7277ca14bb3a3a81 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 11 Aug 2016 17:56:55 +0800 Subject: [PATCH 076/141] Fix mock --- spec/models/merge_request_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 3270b877c1a..11cfc7cace1 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -463,8 +463,8 @@ describe MergeRequest, models: true do allow(subject).to receive(:diff_head_sha).and_return('123abc') - expect(subject.source_project).to receive(:pipeline). - with('123abc', 'master'). + expect(subject.source_project).to receive(:pipeline_for). + with('master', '123abc'). and_return(pipeline) expect(subject.pipeline).to eq(pipeline) From 2a435e1d116065496fcb82f9b2182f7037d4c8b3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 11 Aug 2016 18:04:52 +0800 Subject: [PATCH 077/141] Remove Pipeline#latest in favour of Project#pipeline_for(ref) --- app/models/ci/pipeline.rb | 4 ---- spec/models/ci/pipeline_spec.rb | 28 +++++++--------------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 50f9ee7fc66..9621bddf8dc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -102,10 +102,6 @@ module Ci end end - def latest - project.pipelines.latest_for(ref).first - end - def latest? return false unless ref commit = project.commit(ref) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 556a6e1b59a..2dbc3d985b0 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -483,7 +483,13 @@ describe Ci::Pipeline, models: true do context 'with non-empty project' do let(:project) { create(:project) } - let(:pipeline) { create_pipeline } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha) + end describe '#latest?' do context 'with latest sha' do @@ -503,26 +509,6 @@ describe Ci::Pipeline, models: true do end end end - - describe '#latest' do - let(:previous_pipeline) { create_pipeline } - - before do - previous_pipeline - pipeline - end - - it 'gives the latest pipeline' do - expect(previous_pipeline.latest).to eq(pipeline) - end - end - - def create_pipeline - create(:ci_pipeline, - project: project, - ref: project.default_branch, - sha: project.commit.sha) - end end describe '#manual_actions' do From d84aa560331a646016880e4d2c5c0a3b3d4b32a6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 11 Aug 2016 18:09:26 +0800 Subject: [PATCH 078/141] Make Pipeline.latest_successful_for return the record --- app/models/ci/pipeline.rb | 8 ++------ app/models/project.rb | 2 +- app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- .../projects/repositories/_download_archive.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 9621bddf8dc..0289a51eedd 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,12 +21,8 @@ module Ci after_save :keep_around_commits # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest_successful_for, ->(ref) do - latest_for(ref).success - end - - scope :latest_for, ->(ref) do - where(ref: ref).order(id: :desc).limit(1) + def self.latest_successful_for(ref) + where(ref: ref).order(id: :desc).success.first end def self.truncate_sha(sha) diff --git a/app/models/project.rb b/app/models/project.rb index 2969bec0bf7..7aa34cdeec8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -432,7 +432,7 @@ class Project < ActiveRecord::Base # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) - latest_pipeline = pipelines.latest_successful_for(ref).first + latest_pipeline = pipelines.latest_successful_for(ref) if latest_pipeline latest_pipeline.builds.latest.with_artifacts diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index b096516c627..2029758f30d 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,7 +27,7 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - - pipeline = @project.pipelines.latest_successful_for(branch.name).first + - pipeline = @project.pipelines.latest_successful_for(branch.name) - if pipeline - artifacts = pipeline.builds.latest.with_artifacts - if artifacts.any? diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index d135b448dd7..e7ef0cbaa91 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -14,7 +14,7 @@ %li = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - pipeline = @project.pipelines.latest_successful_for(@ref).first + - pipeline = @project.pipelines.latest_successful_for(@ref) - if pipeline - artifacts = pipeline.builds.latest.with_artifacts - if artifacts.any? diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 183daebfc3a..0ef9ad5f789 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -25,7 +25,7 @@ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.fa.fa-download %span Download tar - - pipeline = @project.pipelines.latest_successful_for(ref).first + - pipeline = @project.pipelines.latest_successful_for(ref) - if pipeline - artifacts = pipeline.builds.latest.with_artifacts - if artifacts.any? diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index bb2e5224306..ba3fd4627af 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -12,7 +12,7 @@ %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - pipeline = project.pipelines.latest_successful_for(ref).first + - pipeline = project.pipelines.latest_successful_for(ref) - if pipeline - artifacts = pipeline.builds.latest.with_artifacts - if artifacts.any? From 62d991c80f0b5716945912ebf2031f56da75a12b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 11 Aug 2016 19:04:45 +0800 Subject: [PATCH 079/141] Show latest pipeline status if what provided artifacts aren't latest --- app/views/projects/branches/_branch.html.haml | 5 ++++- app/views/projects/buttons/_download.html.haml | 5 ++++- app/views/projects/repositories/_download_archive.html.haml | 5 ++++- app/views/projects/tags/_download.html.haml | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 2029758f30d..8a33cd1502c 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -40,7 +40,10 @@ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(@project.namespace, @project, pipeline))})" + - latest_pipeline = @project.pipeline_for(branch.name) + %li + %span= latest_pipeline.status.humanize + %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, branch.name, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index e7ef0cbaa91..84135a6d049 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -20,7 +20,10 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(@project.namespace, @project, pipeline))})" + - latest_pipeline = @project.pipeline_for(@ref) + %li + %span= latest_pipeline.status.humanize + %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, @ref, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 0ef9ad5f789..c9a0b74e47b 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -31,7 +31,10 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(@project.namespace, @project, pipeline))})" + - latest_pipeline = @project.pipeline_for(ref) + %li + %span= latest_pipeline.status.humanize + %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(@project.namespace, @project, ref, 'download', job: job.name), rel: 'nofollow' do diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index ba3fd4627af..10e8f77ac0c 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -18,7 +18,10 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - = " (not latest, but #{link_to(pipeline.short_sha, namespace_project_pipeline_path(project.namespace, project, pipeline))})" + - latest_pipeline = @project.pipeline_for(ref) + %li + %span= latest_pipeline.status.humanize + %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li = link_to search_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do From 44b29b5b3b908ee5acd1d35fdf8e75333e7e50c1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 11 Aug 2016 19:15:22 +0800 Subject: [PATCH 080/141] Add a CHANGELOG entry --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 42d32e53685..407d183c17d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.11.0 (unreleased) - Fix CI status icon link underline (ClemMakesApps) - The Repository class is now instrumented - Cache the commit author in RequestStore to avoid extra lookups in PostReceive + - Add a button to download latest successful artifacts for branches and tags - Expand commit message width in repo view (ClemMakesApps) - Cache highlighted diff lines for merge requests - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' From d79fb3e3ca47f6d6cd7aa81811d884340a0b0a64 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 00:43:06 +0800 Subject: [PATCH 081/141] Fix tests which broke in the merge --- spec/requests/api/commits_spec.rb | 4 ++-- spec/services/ci/image_for_build_service_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 7ca75d77673..5b3dc60aba2 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -95,7 +95,7 @@ describe API::API, api: true do end it "returns status for CI" do - pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline('master', project.repository.commit.sha) pipeline.update(status: 'success') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) @@ -105,7 +105,7 @@ describe API::API, api: true do end it "returns status for CI when pipeline is created" do - project.ensure_pipeline(project.repository.commit.sha, 'master') + project.ensure_pipeline('master', project.repository.commit.sha) get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index c931c3e4829..b3e0a7b9b58 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') } + let(:pipeline) { project.ensure_pipeline('master', commit_sha) } let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } describe '#execute' do From abf1cffff8afd6dcb181e532378ed1548dd62078 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 00:46:51 +0800 Subject: [PATCH 082/141] Fix tests, explicitly set the status --- spec/requests/projects/artifacts_controller_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 952b9fb99b7..61d5e3d9a7d 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -8,7 +8,8 @@ describe Projects::ArtifactsController do create(:ci_pipeline, project: project, sha: project.commit.sha, - ref: project.default_branch) + ref: project.default_branch, + status: 'success') end let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } From 73bbaffbfcfb24942111726ae6e04170f1b61ccc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 01:03:08 +0800 Subject: [PATCH 083/141] Use URL helper, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13880889 --- app/controllers/projects/artifacts_controller.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 05112571225..b7c395a01a3 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -35,10 +35,14 @@ class Projects::ArtifactsController < Projects::ApplicationController end def search - if params[:path] - url = namespace_project_build_url(project.namespace, project, build) + path = params[:path] - redirect_to "#{url}/artifacts/#{params[:path]}" + if %w[download browse file].include?(path) + redirect_to send( + "#{path}_namespace_project_build_artifacts_url", + project.namespace, + project, + build) else render_404 end From 16b63664f9d73a2ab83feac6eadf959f530208c9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 01:15:29 +0800 Subject: [PATCH 084/141] It's project, not @project --- app/views/projects/tags/_download.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 10e8f77ac0c..27e0cd7b1f9 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -18,7 +18,7 @@ - if artifacts.any? %li.dropdown-header Artifacts - unless pipeline.latest? - - latest_pipeline = @project.pipeline_for(ref) + - latest_pipeline = project.pipeline_for(ref) %li %span= latest_pipeline.status.humanize %li.dropdown-header Previous Artifacts From d8dfa56e95c794a91c0a1185e5e6c0017e144b25 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 01:43:58 +0800 Subject: [PATCH 085/141] Fix test by assigning the proper SHA --- spec/features/projects/tags/download_buttons_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb index ebc5204cf1d..6e0022c179f 100644 --- a/spec/features/projects/tags/download_buttons_spec.rb +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -10,7 +10,7 @@ feature 'Download buttons in tags page', feature: true do given(:pipeline) do create(:ci_pipeline, project: project, - sha: project.commit.sha, + sha: project.commit(tag).sha, ref: tag, status: status) end From 1501b1abc4be78eaa4cb4c28969d9bc59bcf284c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 15 Aug 2016 23:48:43 +0200 Subject: [PATCH 086/141] Fixed bug when a pipeline for latest SHA does not exist --- app/helpers/ci_status_helper.rb | 5 +++++ app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- app/views/projects/repositories/_download_archive.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index ea2f5f9281a..0f7fcc0416c 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -25,6 +25,11 @@ module CiStatusHelper end end + def ci_status_for_statuseable(subject) + status = subject.try(:status) || 'not found' + status.humanize + end + def ci_icon_for_status(status) icon_name = case status diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 8a33cd1502c..402e37f4ec6 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -42,7 +42,7 @@ - unless pipeline.latest? - latest_pipeline = @project.pipeline_for(branch.name) %li - %span= latest_pipeline.status.humanize + %span= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 84135a6d049..a86561ca90b 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -22,7 +22,7 @@ - unless pipeline.latest? - latest_pipeline = @project.pipeline_for(@ref) %li - %span= latest_pipeline.status.humanize + %span= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index c9a0b74e47b..e482bb72df4 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -33,7 +33,7 @@ - unless pipeline.latest? - latest_pipeline = @project.pipeline_for(ref) %li - %span= latest_pipeline.status.humanize + %span= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 27e0cd7b1f9..b3bf18cfa50 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -20,7 +20,7 @@ - unless pipeline.latest? - latest_pipeline = project.pipeline_for(ref) %li - %span= latest_pipeline.status.humanize + %span= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li From ad320577595610ee1cb8f945cdfe6f739e9a0ebb Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 15 Aug 2016 19:07:42 -0500 Subject: [PATCH 087/141] Add unclickable state to running build artifacts --- app/assets/stylesheets/framework/dropdowns.scss | 6 ++++++ app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- app/views/projects/repositories/_download_archive.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e8eafa15899..cbbff49ad9c 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -176,6 +176,12 @@ .separator + .dropdown-header { padding-top: 2px; } + + .unclickable { + cursor: not-allowed; + padding: 5px 8px; + color: $dropdown-header-color; + } } .dropdown-menu-large { diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 402e37f4ec6..12b78be4be0 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -42,7 +42,7 @@ - unless pipeline.latest? - latest_pipeline = @project.pipeline_for(branch.name) %li - %span= ci_status_for_statuseable(latest_pipeline) + .unclickable= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index a86561ca90b..45998343fda 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -22,7 +22,7 @@ - unless pipeline.latest? - latest_pipeline = @project.pipeline_for(@ref) %li - %span= ci_status_for_statuseable(latest_pipeline) + .unclickable= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index e482bb72df4..48502498171 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -33,7 +33,7 @@ - unless pipeline.latest? - latest_pipeline = @project.pipeline_for(ref) %li - %span= ci_status_for_statuseable(latest_pipeline) + .unclickable= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index b3bf18cfa50..60f1e2cd2ee 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -20,7 +20,7 @@ - unless pipeline.latest? - latest_pipeline = project.pipeline_for(ref) %li - %span= ci_status_for_statuseable(latest_pipeline) + .unclickable= ci_status_for_statuseable(latest_pipeline) %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li From 11f840bfa5b93fdd0687c9c4f2a5a2e7abbc17ac Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 21:04:06 +0800 Subject: [PATCH 088/141] An empty line after guard, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13904931 --- app/models/merge_request.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index cb60b626e75..96fecf7712a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -676,6 +676,7 @@ class MergeRequest < ActiveRecord::Base def pipeline return unless diff_head_sha && source_project + @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) end From f86a507745695f3b073d6edb6029836ab115765a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 22:10:10 +0800 Subject: [PATCH 089/141] Rename to latest_succeeded, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13908017 --- app/controllers/projects/artifacts_controller.rb | 2 +- app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/buttons/_download.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- config/routes.rb | 7 ++++--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index b7c395a01a3..5cc6d643b64 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -34,7 +34,7 @@ class Projects::ArtifactsController < Projects::ApplicationController redirect_to namespace_project_build_path(project.namespace, project, build) end - def search + def latest_succeeded path = params[:path] if %w[download browse file].include?(path) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 12b78be4be0..5634abf3641 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -46,7 +46,7 @@ %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li - = link_to search_namespace_project_artifacts_path(@project.namespace, @project, branch.name, 'download', job: job.name), rel: 'nofollow' do + = link_to latest_succeeded_namespace_project_artifacts_path(@project.namespace, @project, branch.name, 'download', job: job.name), rel: 'nofollow' do %span Download '#{job.name}' - if can_remove_branch?(@project, branch.name) diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 45998343fda..177946dcd42 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -26,5 +26,5 @@ %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li - = link_to search_namespace_project_artifacts_path(@project.namespace, @project, @ref, 'download', job: job.name), rel: 'nofollow' do + = link_to latest_succeeded_namespace_project_artifacts_path(@project.namespace, @project, @ref, 'download', job: job.name), rel: 'nofollow' do %span Download '#{job.name}' diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 60f1e2cd2ee..316d03dd12a 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -24,5 +24,5 @@ %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li - = link_to search_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do %span Download '#{job.name}' diff --git a/config/routes.rb b/config/routes.rb index 70bdb1d5beb..da10c5609f6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -765,9 +765,10 @@ Rails.application.routes.draw do resources :artifacts, only: [] do collection do - get :search, path: ':ref_name/*path', - format: false, - constraints: { ref_name: /.+/ } # could have / + get :latest_succeeded, + path: ':ref_name/*path', + format: false, + constraints: { ref_name: /.+/ } # could have / end end end From 1c88ed7a515141b7468b238a679e2ff5cf86e3f9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 16 Aug 2016 22:14:27 +0800 Subject: [PATCH 090/141] Not sure why missed this one --- app/views/projects/repositories/_download_archive.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 48502498171..c0a5909d3a6 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -37,5 +37,5 @@ %li.dropdown-header Previous Artifacts - artifacts.each do |job| %li - = link_to search_namespace_project_artifacts_path(@project.namespace, @project, ref, 'download', job: job.name), rel: 'nofollow' do + = link_to latest_succeeded_namespace_project_artifacts_path(@project.namespace, @project, ref, 'download', job: job.name), rel: 'nofollow' do %span Download '#{job.name}' From 75df5f6c73670def1912150c6f3390ffdfadb17a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 17 Aug 2016 13:42:06 +0800 Subject: [PATCH 091/141] Fixed a missing rename --- spec/requests/projects/artifacts_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 61d5e3d9a7d..1c68ec9117b 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -25,7 +25,7 @@ describe Projects::ArtifactsController do def path_from_ref( ref = pipeline.ref, job = build.name, path = 'browse') - search_namespace_project_artifacts_path( + latest_succeeded_namespace_project_artifacts_path( project.namespace, project, ref, From ee33b3e6e84bb566e84062e70d45c6d84ace4ee7 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 17 Aug 2016 17:06:31 +0800 Subject: [PATCH 092/141] Use partials for downloading artifacts button --- app/views/projects/branches/_branch.html.haml | 30 ++++++------------- .../projects/buttons/_artifacts.html.haml | 14 +++++++++ .../projects/buttons/_download.html.haml | 15 +--------- .../repositories/_download_archive.html.haml | 15 +--------- app/views/projects/tags/_download.html.haml | 15 +--------- 5 files changed, 26 insertions(+), 63 deletions(-) create mode 100644 app/views/projects/buttons/_artifacts.html.haml diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 5634abf3641..21ac675ffe0 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,27 +27,15 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - - pipeline = @project.pipelines.latest_successful_for(branch.name) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - .dropdown.inline.artifacts-btn - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - = icon('download') - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = @project.pipeline_for(branch.name) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(@project.namespace, @project, branch.name, 'download', job: job.name), rel: 'nofollow' do - %span Download '#{job.name}' + - if @project.latest_successful_builds_for(branch.name).any? + .dropdown.inline.artifacts-btn + %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + = icon('download') + %span.caret + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + = render 'projects/buttons/artifacts', project: @project, ref: branch.name - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do diff --git a/app/views/projects/buttons/_artifacts.html.haml b/app/views/projects/buttons/_artifacts.html.haml new file mode 100644 index 00000000000..a52677ebf0a --- /dev/null +++ b/app/views/projects/buttons/_artifacts.html.haml @@ -0,0 +1,14 @@ +- pipeline = project.pipelines.latest_successful_for(ref) +- if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do + %span Download '#{job.name}' diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 177946dcd42..5e748c44b08 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -14,17 +14,4 @@ %li = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - pipeline = @project.pipelines.latest_successful_for(@ref) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = @project.pipeline_for(@ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(@project.namespace, @project, @ref, 'download', job: job.name), rel: 'nofollow' do - %span Download '#{job.name}' + = render 'projects/buttons/artifacts', project: @project, ref: @ref diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index c0a5909d3a6..4f40696a190 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -25,17 +25,4 @@ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.fa.fa-download %span Download tar - - pipeline = @project.pipelines.latest_successful_for(ref) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = @project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(@project.namespace, @project, ref, 'download', job: job.name), rel: 'nofollow' do - %span Download '#{job.name}' + = render 'projects/buttons/artifacts', project: @project, ref: ref diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 316d03dd12a..6985eb74ca7 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -12,17 +12,4 @@ %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %span Download tar.gz - - pipeline = project.pipelines.latest_successful_for(ref) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do - %span Download '#{job.name}' + = render 'projects/buttons/artifacts', project: project, ref: ref From 4fbe044b74aa6a24133732ef8a9bc5063ecef5dd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 17 Aug 2016 17:38:38 +0800 Subject: [PATCH 093/141] Use switch case in a helper, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13988401 --- app/controllers/projects/artifacts_controller.rb | 10 +++------- app/helpers/gitlab_routing_helper.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 5cc6d643b64..8261a73c642 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -35,14 +35,10 @@ class Projects::ArtifactsController < Projects::ApplicationController end def latest_succeeded - path = params[:path] + target_url = artifacts_action_url(params[:path], project, build) - if %w[download browse file].include?(path) - redirect_to send( - "#{path}_namespace_project_build_artifacts_url", - project.namespace, - project, - build) + if target_url + redirect_to(target_url) else render_404 end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 5386ddadc62..bc4d976ae68 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -149,4 +149,19 @@ module GitlabRoutingHelper def resend_invite_group_member_path(group_member, *args) resend_invite_group_group_member_path(group_member.source, group_member) end + + # Artifacts + + def artifacts_action_url(path, project, build) + args = [project.namespace, project, build] + + case path + when 'download' + download_namespace_project_build_artifacts_url(*args) + when 'browse' + browse_namespace_project_build_artifacts_url(*args) + when 'file' + file_namespace_project_build_artifacts_url(*args) + end + end end From e8b03baf6b67a14c0db6dbf3a1abaa4a6a173213 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 18 Aug 2016 15:31:20 +0800 Subject: [PATCH 094/141] Use path rather than URL because it should work for http 302: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14035941 --- app/controllers/projects/artifacts_controller.rb | 4 ++-- app/helpers/gitlab_routing_helper.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 8261a73c642..16ab7ec409d 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -35,10 +35,10 @@ class Projects::ArtifactsController < Projects::ApplicationController end def latest_succeeded - target_url = artifacts_action_url(params[:path], project, build) + target_path = artifacts_action_path(params[:path], project, build) if target_url - redirect_to(target_url) + redirect_to(target_path) else render_404 end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index bc4d976ae68..cd526f17b99 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -152,16 +152,16 @@ module GitlabRoutingHelper # Artifacts - def artifacts_action_url(path, project, build) + def artifacts_action_path(path, project, build) args = [project.namespace, project, build] case path when 'download' - download_namespace_project_build_artifacts_url(*args) + download_namespace_project_build_artifacts_path(*args) when 'browse' - browse_namespace_project_build_artifacts_url(*args) + browse_namespace_project_build_artifacts_path(*args) when 'file' - file_namespace_project_build_artifacts_url(*args) + file_namespace_project_build_artifacts_path(*args) end end end From 17d0406546885bedf2196c61a5991092b3fbe7c0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 18 Aug 2016 19:30:20 +0800 Subject: [PATCH 095/141] Not sure why I missed this renaming --- app/controllers/projects/artifacts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 16ab7ec409d..60e432d68d8 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController def latest_succeeded target_path = artifacts_action_path(params[:path], project, build) - if target_url + if target_path redirect_to(target_path) else render_404 From 1aba3a5c0e7c2b727f4317aab19bbc662f0fd727 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 19 Aug 2016 17:42:49 +0800 Subject: [PATCH 096/141] Unify pipeline_for(ref, nil) and pipeline_for(ref), feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14073464 --- app/models/project.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 678fca7afd1..e97e6abfef9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1094,8 +1094,11 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def pipeline_for(ref, sha = commit(ref).try(:sha)) + def pipeline_for(ref, sha = nil) + sha ||= commit(ref).try(:sha) + return unless sha + pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end From eadb1dc547ac1d0e3eb964d077f0b1580d588a95 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 19 Aug 2016 18:46:15 +0800 Subject: [PATCH 097/141] Make sure the branch we're testing is on the 1st page! --- spec/features/projects/branches/download_buttons_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index a223786777b..04058300570 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -9,8 +9,8 @@ feature 'Download buttons in branches page', feature: true do given(:pipeline) do create(:ci_pipeline, project: project, - sha: project.commit.sha, - ref: project.default_branch, + sha: project.commit('binary-encoding').sha, + ref: 'binary-encoding', # make sure the branch is in the 1st page! status: status) end From bc3493f9474d2557c1c30bf30a61e4cd51ece0f1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 24 Aug 2016 14:40:18 +0800 Subject: [PATCH 098/141] Use only one before block, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142/diffs#note_14347758 --- spec/requests/projects/artifacts_controller_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 1c68ec9117b..3ba6725efc3 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -17,9 +17,7 @@ describe Projects::ArtifactsController do describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do before do project.team << [user, :developer] - end - before do login_as(user) end From e65bc0f175c54d9df66fd4950972c0b0b08d448e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 24 Aug 2016 16:02:56 +0800 Subject: [PATCH 099/141] Path could also have slashes! Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14347729 --- .../projects/artifacts_controller.rb | 14 +++++++++--- app/helpers/gitlab_routing_helper.rb | 5 +++-- config/routes.rb | 5 ++--- .../projects/artifacts_controller_spec.rb | 22 +++++++++++++++++-- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 60e432d68d8..17c6d56c8b9 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,4 +1,6 @@ class Projects::ArtifactsController < Projects::ApplicationController + include ExtractsPath + layout 'project' before_action :authorize_read_build! before_action :authorize_update_build!, only: [:keep] @@ -35,7 +37,8 @@ class Projects::ArtifactsController < Projects::ApplicationController end def latest_succeeded - target_path = artifacts_action_path(params[:path], project, build) + path = ref_name_and_path.last + target_path = artifacts_action_path(path, project, build) if target_path redirect_to(target_path) @@ -59,13 +62,18 @@ class Projects::ArtifactsController < Projects::ApplicationController end def build_from_ref - if params[:ref_name] - builds = project.latest_successful_builds_for(params[:ref_name]) + if params[:ref_name_and_path] + ref_name = ref_name_and_path.first + builds = project.latest_successful_builds_for(ref_name) builds.find_by(name: params[:job]) end end + def ref_name_and_path + @ref_name_and_path ||= extract_ref(params[:ref_name_and_path]) + end + def artifacts_file @artifacts_file ||= build.artifacts_file end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index cd526f17b99..a322a90cc4e 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -153,9 +153,10 @@ module GitlabRoutingHelper # Artifacts def artifacts_action_path(path, project, build) - args = [project.namespace, project, build] + action, path_params = path.split('/', 2) + args = [project.namespace, project, build, path_params] - case path + case action when 'download' download_namespace_project_build_artifacts_path(*args) when 'browse' diff --git a/config/routes.rb b/config/routes.rb index 606181ff837..879cd61a02f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -787,9 +787,8 @@ Rails.application.routes.draw do resources :artifacts, only: [] do collection do get :latest_succeeded, - path: ':ref_name/*path', - format: false, - constraints: { ref_name: /.+/ } # could have / + path: '*ref_name_and_path', + format: false end end end diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb index 3ba6725efc3..e02f0eacc93 100644 --- a/spec/requests/projects/artifacts_controller_spec.rb +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -26,8 +26,7 @@ describe Projects::ArtifactsController do latest_succeeded_namespace_project_artifacts_path( project.namespace, project, - ref, - path, + [ref, path].join('/'), job: job) end @@ -94,6 +93,25 @@ describe Projects::ArtifactsController do it_behaves_like 'redirect to the build' end + + context 'with branch name and path containing slashes' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome', build.name, 'file/README.md') + end + + it 'redirects' do + path = file_namespace_project_build_artifacts_path( + project.namespace, + project, + build, + 'README.md') + + expect(response).to redirect_to(path) + end + end end end end From 8f197315b3ec354cb0cc0af4acbe54d6aa01d71b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 24 Aug 2016 19:09:10 +0800 Subject: [PATCH 100/141] Aggressively merge views, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14347679 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14347470 --- app/assets/stylesheets/framework/lists.scss | 4 -- app/views/projects/branches/_branch.html.haml | 10 +---- .../projects/buttons/_artifacts.html.haml | 14 ------- .../projects/buttons/_download.html.haml | 38 +++++++++++++++---- .../repositories/_download_archive.html.haml | 28 -------------- app/views/projects/show.html.haml | 4 +- app/views/projects/tags/_download.html.haml | 15 -------- app/views/projects/tags/_tag.html.haml | 3 +- app/views/projects/tags/show.html.haml | 3 +- app/views/projects/tree/show.html.haml | 3 +- 10 files changed, 37 insertions(+), 85 deletions(-) delete mode 100644 app/views/projects/buttons/_artifacts.html.haml delete mode 100644 app/views/projects/repositories/_download_archive.html.haml delete mode 100644 app/views/projects/tags/_download.html.haml diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index a88c7906f5d..965fcc06518 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -162,10 +162,6 @@ ul.content-list { margin-right: 0; } } - - .artifacts-btn { - margin-right: 10px; - } } // When dragging a list item diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 21ac675ffe0..c5549f86e38 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,15 +27,7 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare - - if @project.latest_successful_builds_for(branch.name).any? - .dropdown.inline.artifacts-btn - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - = icon('download') - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - = render 'projects/buttons/artifacts', project: @project, ref: branch.name + = render 'projects/buttons/download', project: @project, ref: branch.name - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do diff --git a/app/views/projects/buttons/_artifacts.html.haml b/app/views/projects/buttons/_artifacts.html.haml deleted file mode 100644 index a52677ebf0a..00000000000 --- a/app/views/projects/buttons/_artifacts.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- pipeline = project.pipelines.latest_successful_for(ref) -- if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do - %span Download '#{job.name}' diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 5e748c44b08..73dcb9c079e 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,17 +1,41 @@ -- unless @project.empty_repo? - - if can? current_user, :download_code, @project - .dropdown.inline.btn-group +- if !project.empty_repo? && can?(current_user, :download_code, project) + %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'} + .dropdown.inline %button.btn{ 'data-toggle' => 'dropdown' } = icon('download') - = icon('caret-down') + %span.caret %span.sr-only Select Archive Format %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li.dropdown-header Source code %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download %span Download zip %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'tar.gz'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download %span Download tar.gz - = render 'projects/buttons/artifacts', project: @project, ref: @ref + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar + + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do + %span Download '#{job.name}' diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml deleted file mode 100644 index 4f40696a190..00000000000 --- a/app/views/projects/repositories/_download_archive.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- ref = ref || nil -- btn_class = btn_class || '' -%span.btn-group{class: btn_class} - .dropdown.inline - %button.btn{ 'data-toggle' => 'dropdown' } - = icon('download') - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Source code - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar - = render 'projects/buttons/artifacts', project: @project, ref: ref diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index a666d07e9eb..7130ebaa743 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -70,7 +70,7 @@ = render 'shared/members/access_request_buttons', source: @project .btn-group.project-repo-btn-group - = render "projects/buttons/download" + = render 'projects/buttons/download', project: @project, ref: @ref = render 'projects/buttons/dropdown' = render 'shared/notifications/button', notification_setting: @notification_setting @@ -86,4 +86,4 @@ Archived project! Repository is read-only %div{class: "project-show-#{default_project_view}"} - = render default_project_view \ No newline at end of file + = render default_project_view diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml deleted file mode 100644 index 6985eb74ca7..00000000000 --- a/app/views/projects/tags/_download.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -.dropdown.inline - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - = icon('download') - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Source code - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %span Download tar.gz - = render 'projects/buttons/artifacts', project: project, ref: ref diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 2c11c0e5b21..a156d98bab8 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -11,8 +11,7 @@ = strip_gpg_signature(tag.message) .controls - - if can?(current_user, :download_code, @project) - = render 'projects/tags/download', ref: tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: tag.name - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 395d7af6cbb..4dd7439b2d0 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -12,8 +12,7 @@ = icon('files-o') = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do = icon('history') - - if can? current_user, :download_code, @project - = render 'projects/tags/download', ref: @tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .pull-right = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index c68f86f1378..37d341212af 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -10,8 +10,7 @@ %div{ class: container_class } .tree-controls = render 'projects/find_file_link' - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped' + = render 'projects/buttons/download', project: @project, ref: @ref #tree-holder.tree-holder.clearfix .nav-block From 6953d988ab141863cba4c38c52b6d1af23c9af3e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 24 Aug 2016 20:57:47 +0800 Subject: [PATCH 101/141] Update CHANGELOG from v8.11.0 to v8.12.0 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 549b86f76ea..33eecc2c9fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.12.0 (unreleased) - Change merge_error column from string to text type - Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Added tests for diff notes + - Add a button to download latest successful artifacts for branches and tags !5142 v 8.11.1 (unreleased) - Fix file links on project page when default view is Files !5933 @@ -42,7 +43,6 @@ v 8.11.0 - Do not escape URI when extracting path !5878 (winniehell) - Fix filter label tooltip HTML rendering (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - - Add a button to download latest successful artifacts for branches and tags - Expand commit message width in repo view (ClemMakesApps) - Cache highlighted diff lines for merge requests - Pre-create all builds for a Pipeline when the new Pipeline is created !5295 From dd8afbf05d0727a061e8d7bc1bc3c1db5a666116 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 25 Aug 2016 15:04:15 +0800 Subject: [PATCH 102/141] Just use instance variable instead, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14400736 --- app/controllers/projects/artifacts_controller.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 17c6d56c8b9..4c63bec90e5 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -37,8 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController end def latest_succeeded - path = ref_name_and_path.last - target_path = artifacts_action_path(path, project, build) + target_path = artifacts_action_path(@path, project, build) if target_path redirect_to(target_path) @@ -63,17 +62,13 @@ class Projects::ArtifactsController < Projects::ApplicationController def build_from_ref if params[:ref_name_and_path] - ref_name = ref_name_and_path.first + ref_name, @path = extract_ref(params[:ref_name_and_path]) builds = project.latest_successful_builds_for(ref_name) builds.find_by(name: params[:job]) end end - def ref_name_and_path - @ref_name_and_path ||= extract_ref(params[:ref_name_and_path]) - end - def artifacts_file @artifacts_file ||= build.artifacts_file end From 6a2d2bd18d0a07c33200d74a09b7cf9adcb7a84d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 25 Aug 2016 15:41:37 +0800 Subject: [PATCH 103/141] Add a download icon for artifacts, too. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14400694 --- app/views/projects/buttons/_download.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 73dcb9c079e..5f5e071eb40 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -38,4 +38,5 @@ - artifacts.each do |job| %li = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do + %i.fa.fa-download %span Download '#{job.name}' From 01a1e3925acf01b925ad3b8ca370db30384111d8 Mon Sep 17 00:00:00 2001 From: Miroslav Meca Date: Thu, 25 Aug 2016 12:01:52 +0000 Subject: [PATCH 104/141] Update projects.md The wrong example for "Branches". Added option parameters in protect branch section. Here is reason: https://gitlab.com/gitlab-org/gitlab-ee/commit/3ab07b8aae8dae43cfa3aae1306c59ea264a8594 Maybe this section could/should be deleted. Because in file repositories.md it had been deleted: https://gitlab.com/gitlab-org/gitlab-ee/commit/8f3701eff005aeedcebff8ce02074f5056a369b3 --- doc/api/projects.md | 54 ++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index 0e4806e31c5..eacdb1e1ee6 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -998,6 +998,8 @@ is available before it is returned in the JSON response or an empty response is ## Branches +For more information please consult the [Branches](branches.md) documentation. + ### List branches Lists all branches of a project. @@ -1016,56 +1018,46 @@ Parameters: "name": "async", "commit": { "id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca", - "parents": [ - { - "id": "3f94fc7c85061973edc9906ae170cc269b07ca55" - } + "parent_ids": [ + "3f94fc7c85061973edc9906ae170cc269b07ca55" ], - "tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee", "message": "give Caolan credit where it's due (up top)", - "author": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, - "committer": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, + "author_name": "Jeremy Ashkenas", + "author_email": "jashkenas@example.com", "authored_date": "2010-12-08T21:28:50+00:00", + "committer_name": "Jeremy Ashkenas", + "committer_email": "jashkenas@example.com", "committed_date": "2010-12-08T21:28:50+00:00" }, - "protected": false + "protected": false, + "developers_can_push": false, + "developers_can_merge": false }, { "name": "gh-pages", "commit": { "id": "101c10a60019fe870d21868835f65c25d64968fc", - "parents": [ - { - "id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa" - } + "parent_ids": [ + "9c15d2e26945a665131af5d7b6d30a06ba338aaa" ], - "tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a", "message": "Underscore.js 1.5.2", - "author": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, - "committer": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, + "author_name": "Jeremy Ashkenas", + "author_email": "jashkenas@example.com", "authored_date": "2013-09-07T12:58:21+00:00", + "committer_name": "Jeremy Ashkenas", + "committer_email": "jashkenas@example.com", "committed_date": "2013-09-07T12:58:21+00:00" }, - "protected": false + "protected": false, + "developers_can_push": false, + "developers_can_merge": false } ] ``` -### List single branch +### Single branch -Lists a specific branch of a project. +A specific branch of a project. ``` GET /projects/:id/repository/branches/:branch @@ -1075,6 +1067,8 @@ Parameters: - `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project - `branch` (required) - The name of the branch. +- `developers_can_push` - Flag if developers can push to the branch. +- `developers_can_merge` - Flag if developers can merge to the branch. ### Protect single branch From b17df0507b704a347d6a8af035526b642ec85284 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 26 Aug 2016 13:10:03 +0800 Subject: [PATCH 105/141] Extract ref_name and path in before_action, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_14469768 --- app/controllers/projects/artifacts_controller.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 4c63bec90e5..59222637961 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -4,6 +4,7 @@ class Projects::ArtifactsController < Projects::ApplicationController layout 'project' before_action :authorize_read_build! before_action :authorize_update_build!, only: [:keep] + before_action :extract_ref_name_and_path before_action :validate_artifacts! def download @@ -48,6 +49,12 @@ class Projects::ArtifactsController < Projects::ApplicationController private + def extract_ref_name_and_path + return unless params[:ref_name_and_path] + + @ref_name, @path = extract_ref(params[:ref_name_and_path]) + end + def validate_artifacts! render_404 unless build && build.artifacts? end @@ -61,12 +68,10 @@ class Projects::ArtifactsController < Projects::ApplicationController end def build_from_ref - if params[:ref_name_and_path] - ref_name, @path = extract_ref(params[:ref_name_and_path]) - builds = project.latest_successful_builds_for(ref_name) + return unless @ref_name - builds.find_by(name: params[:job]) - end + builds = project.latest_successful_builds_for(@ref_name) + builds.find_by(name: params[:job]) end def artifacts_file From 1fc0d4f4b50a9aeae73a0b0a9a4641e57786e6b3 Mon Sep 17 00:00:00 2001 From: Jake Romer Date: Fri, 26 Aug 2016 06:12:30 +0000 Subject: [PATCH 106/141] Clarify blank line rule in newlines_styleguide.md To clarify what's meant by "from a logical perspective" here, I consulted Python's PEP8 style guide, which provides some helpfully precise language: > Extra blank lines may be used (sparingly) to separate groups of > related functions. Blank lines may be omitted between a bunch of > related one-liners (e.g. a set of dummy implementations). https://www.python.org/dev/peps/pep-0008/#blank-lines I adapted this passage to the existing language for the newline rule. --- doc/development/newlines_styleguide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md index e03adcaadea..32aac2529a4 100644 --- a/doc/development/newlines_styleguide.md +++ b/doc/development/newlines_styleguide.md @@ -2,7 +2,7 @@ This style guide recommends best practices for newlines in Ruby code. -## Rule: separate code with newlines only when it makes sense from logic perspectice +## Rule: separate code with newlines only to group together related logic ```ruby # bad From c40fd0b1c2aa5d3354fd50e37a4fe22483df2042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20=C3=98ivind=20Bj=C3=B8rnsen?= Date: Fri, 26 Aug 2016 07:24:13 +0000 Subject: [PATCH 107/141] docs: make sure to update 8.10-to-8.11 workhorse version too (see !5983) --- doc/update/8.10-to-8.11.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index 98721763566..b058f8e2a03 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -82,7 +82,7 @@ GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout v0.7.8 +sudo -u git -H git checkout v0.7.11 sudo -u git -H make ``` From 3a8fb95c98262986ec09655bf5572c3e3d2145d6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 26 Aug 2016 15:40:12 +0800 Subject: [PATCH 108/141] Fix tests --- lib/gitlab/badge/coverage/report.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb index 95d925dc7f3..9a0482306b7 100644 --- a/lib/gitlab/badge/coverage/report.rb +++ b/lib/gitlab/badge/coverage/report.rb @@ -12,9 +12,7 @@ module Gitlab @ref = ref @job = job - @pipeline = @project.pipelines - .latest_successful_for(@ref) - .first + @pipeline = @project.pipelines.latest_successful_for(@ref) end def entity From 41a0b7b22f7cdec7d216f32d561442c9fc3587be Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 26 Aug 2016 17:32:00 +0800 Subject: [PATCH 109/141] Fix CHANGELOG --- CHANGELOG | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index db3e891bdcd..a262dc54e5b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,8 +15,6 @@ v 8.12.0 (unreleased) - Fix groups sort dropdown alignment (ClemMakesApps) - Added tests for diff notes - Add a button to download latest successful artifacts for branches and tags !5142 - -v 8.11.1 (unreleased) - Add delimiter to project stars and forks count (ClemMakesApps) - Fix badge count alignment (ClemMakesApps) - Fix branch title trailing space on hover (ClemMakesApps) From 6686084c6502f10fd7e6b8963ab52526cb6831bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Fri, 26 Aug 2016 17:20:00 -0300 Subject: [PATCH 110/141] Fix "Wiki" link not appearing in navigation for projects with external wiki --- CHANGELOG | 1 + app/models/ability.rb | 2 +- app/models/project.rb | 4 ++++ spec/models/ability_spec.rb | 13 +++++++++++++ spec/models/project_spec.rb | 12 ++++++++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7817470d95e..b5ae1adf1d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,6 +37,7 @@ v 8.11.3 (unreleased) - Allow system info page to handle case where info is unavailable - Label list shows all issues (opened or closed) with that label - Don't show resolve conflicts link before MR status is updated + - Fix "Wiki" link not appearing in navigation for projects with external wiki - Fix IE11 fork button bug !598 - Don't prevent viewing the MR when git refs for conflicts can't be found on disk - Fix external issue tracker "Issues" link leading to 404s diff --git a/app/models/ability.rb b/app/models/ability.rb index a49dd703926..c1df4a865f6 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -355,7 +355,7 @@ class Ability rules += named_abilities('project_snippet') end - unless project.wiki_enabled + unless project.has_wiki? rules += named_abilities('wiki') end diff --git a/app/models/project.rb b/app/models/project.rb index 0e4fb94f8eb..0fa41ebbec3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -680,6 +680,10 @@ class Project < ActiveRecord::Base update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) end + def has_wiki? + wiki_enabled? || has_external_wiki? + end + def external_wiki if has_external_wiki.nil? cache_has_external_wiki # Populate diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index aa3b2bbf471..c50ca38bdd9 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -282,4 +282,17 @@ describe Ability, lib: true do end end end + + describe '.project_disabled_features_rules' do + let(:project) { build(:project) } + + subject { described_class.project_disabled_features_rules(project) } + + context 'wiki named abilities' do + it 'disables wiki abilities if the project has no wiki' do + expect(project).to receive(:has_wiki?).and_return(false) + expect(subject).to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b2baeeb31bb..3b637b0defc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -506,6 +506,18 @@ describe Project, models: true do end end + describe '#has_wiki?' do + let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } + let(:wiki_enabled_project) { build(:project, wiki_enabled: true) } + let(:external_wiki_project) { build(:project, has_external_wiki: true) } + + it 'returns true if project is wiki enabled or has external wiki' do + expect(wiki_enabled_project).to have_wiki + expect(external_wiki_project).to have_wiki + expect(no_wiki_project).not_to have_wiki + end + end + describe '#external_wiki' do let(:project) { create(:project) } From 9c2d061ad468e6a47d21617fb2c6b874e22c13bc Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 24 Aug 2016 11:43:44 +0100 Subject: [PATCH 111/141] Drop unused CI tables and files These tables, web hooks and services, are unused but where not dropped with the commits d5c91bb9a601a1a344d94763654f0b0996857497 and 2988e1fbf50b3c9e803a9358933e3e969e64dcc3. The file was left too, but never called. --- app/services/ci/web_hook_service.rb | 35 ------------------- .../20160824103857_drop_unused_ci_tables.rb | 11 ++++++ db/schema.rb | 19 +--------- 3 files changed, 12 insertions(+), 53 deletions(-) delete mode 100644 app/services/ci/web_hook_service.rb create mode 100644 db/migrate/20160824103857_drop_unused_ci_tables.rb diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb deleted file mode 100644 index 92e6df442b4..00000000000 --- a/app/services/ci/web_hook_service.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Ci - class WebHookService - def build_end(build) - execute_hooks(build.project, build_data(build)) - end - - def execute_hooks(project, data) - project.web_hooks.each do |web_hook| - async_execute_hook(web_hook, data) - end - end - - def async_execute_hook(hook, data) - Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data) - end - - def build_data(build) - project = build.project - data = {} - data.merge!({ - build_id: build.id, - build_name: build.name, - build_status: build.status, - build_started_at: build.started_at, - build_finished_at: build.finished_at, - project_id: project.id, - project_name: project.name, - gitlab_url: project.gitlab_url, - ref: build.ref, - before_sha: build.before_sha, - sha: build.sha, - }) - end - end -end diff --git a/db/migrate/20160824103857_drop_unused_ci_tables.rb b/db/migrate/20160824103857_drop_unused_ci_tables.rb new file mode 100644 index 00000000000..65cf46308d9 --- /dev/null +++ b/db/migrate/20160824103857_drop_unused_ci_tables.rb @@ -0,0 +1,11 @@ +class DropUnusedCiTables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + drop_table(:ci_services) + drop_table(:ci_web_hooks) + end +end diff --git a/db/schema.rb b/db/schema.rb index 5a105a91ad1..227e10294e4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160823081327) do +ActiveRecord::Schema.define(version: 20160824103857) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -295,16 +295,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree - create_table "ci_services", force: :cascade do |t| - t.string "type" - t.string "title" - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false - t.text "properties" - end - create_table "ci_sessions", force: :cascade do |t| t.string "session_id", null: false t.text "data" @@ -360,13 +350,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree - create_table "ci_web_hooks", force: :cascade do |t| - t.string "url", null: false - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false From b125006517db2b20a29ebbb9e7ad6f6ef03a216f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Aug 2016 09:20:53 +0200 Subject: [PATCH 112/141] Do not enforce using a hash with hidden ci key --- lib/gitlab/ci/config/node/hidden_job.rb | 1 - spec/lib/gitlab/ci/config/node/hidden_job_spec.rb | 15 ++------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden_job.rb index 073044b66f8..19514a653b0 100644 --- a/lib/gitlab/ci/config/node/hidden_job.rb +++ b/lib/gitlab/ci/config/node/hidden_job.rb @@ -9,7 +9,6 @@ module Gitlab include Validatable validations do - validates :config, type: Hash validates :config, presence: true end diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb index cc44e2cc054..ddc39405bbe 100644 --- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb @@ -5,11 +5,11 @@ describe Gitlab::Ci::Config::Node::HiddenJob do describe 'validations' do context 'when entry config value is correct' do - let(:config) { { image: 'ruby:2.2' } } + let(:config) { [:some, :array] } describe '#value' do it 'returns key value' do - expect(entry.value).to eq(image: 'ruby:2.2') + expect(entry.value).to eq [:some, :array] end end @@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do end context 'when entry value is not correct' do - context 'incorrect config value type' do - let(:config) { ['incorrect'] } - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'hidden job config should be a hash' - end - end - end - context 'when config is empty' do let(:config) { {} } From 2991f93f2fdd2b6de2b307ee5ba8d8ac6651f845 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Aug 2016 09:30:48 +0200 Subject: [PATCH 113/141] Rename CI config hidden job entry to hidden entry --- lib/gitlab/ci/config/node/{hidden_job.rb => hidden.rb} | 2 +- lib/gitlab/ci/config/node/jobs.rb | 2 +- .../ci/config/node/{hidden_job_spec.rb => hidden_spec.rb} | 2 +- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename lib/gitlab/ci/config/node/{hidden_job.rb => hidden.rb} (91%) rename spec/lib/gitlab/ci/config/node/{hidden_job_spec.rb => hidden_spec.rb} (95%) diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden.rb similarity index 91% rename from lib/gitlab/ci/config/node/hidden_job.rb rename to lib/gitlab/ci/config/node/hidden.rb index 19514a653b0..fe4ee8a7fc6 100644 --- a/lib/gitlab/ci/config/node/hidden_job.rb +++ b/lib/gitlab/ci/config/node/hidden.rb @@ -5,7 +5,7 @@ module Gitlab ## # Entry that represents a hidden CI/CD job. # - class HiddenJob < Entry + class Hidden < Entry include Validatable validations do diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 51683c82ceb..a1a26d4fd8f 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -30,7 +30,7 @@ module Gitlab def compose! @config.each do |name, config| - node = hidden?(name) ? Node::HiddenJob : Node::Job + node = hidden?(name) ? Node::Hidden : Node::Job factory = Node::Factory.new(node) .value(config || {}) diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb similarity index 95% rename from spec/lib/gitlab/ci/config/node/hidden_job_spec.rb rename to spec/lib/gitlab/ci/config/node/hidden_spec.rb index ddc39405bbe..61e2a554419 100644 --- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/hidden_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::HiddenJob do +describe Gitlab::Ci::Config::Node::Hidden do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index b8d9c70479c..ae2c88aac37 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -74,7 +74,7 @@ describe Gitlab::Ci::Config::Node::Jobs do expect(entry.descendants.first(2)) .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) expect(entry.descendants.last) - .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob) + .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden) end end From 173e0a7c7cc1a6f81f3ef3e9395051ca11294a4e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Aug 2016 09:35:19 +0200 Subject: [PATCH 114/141] Update documentation about hidden keys in CI YAML --- doc/ci/yaml/README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e7850aa2c9d..58d5306f12a 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -39,7 +39,7 @@ If you want a quick introduction to GitLab CI, follow our - [before_script and after_script](#before_script-and-after_script) - [Git Strategy](#git-strategy) - [Shallow cloning](#shallow-cloning) -- [Hidden jobs](#hidden-jobs) +- [Hidden keys](#hidden-keys) - [Special YAML features](#special-yaml-features) - [Anchors](#anchors) - [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) @@ -934,24 +934,27 @@ variables: GIT_DEPTH: "3" ``` -## Hidden jobs +## Hidden keys >**Note:** Introduced in GitLab 8.6 and GitLab Runner v1.1.1. -Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can +Keys that start with a dot (`.`) will be not processed by GitLab CI. You can use this feature to ignore jobs, or use the -[special YAML features](#special-yaml-features) and transform the hidden jobs +[special YAML features](#special-yaml-features) and transform the hidden keys into templates. -In the following example, `.job_name` will be ignored: +In the following example, `.key_name` will be ignored: ```yaml -.job_name: +.key_name: script: - rake spec ``` +Hidden keys can be hashes like normal CI jobs, but you are also allowed to use +different types of structures to leverage special YAML features. + ## Special YAML features It's possible to use special YAML features like anchors (`&`), aliases (`*`) @@ -967,7 +970,7 @@ Introduced in GitLab 8.6 and GitLab Runner v1.1.1. YAML also has a handy feature called 'anchors', which let you easily duplicate content across your document. Anchors can be used to duplicate/inherit -properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs) +properties, and is a perfect example to be used with [hidden keys](#hidden-keys) to provide templates for your jobs. The following example uses anchors and map merging. It will create two jobs, @@ -975,7 +978,7 @@ The following example uses anchors and map merging. It will create two jobs, having their own custom `script` defined: ```yaml -.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition' +.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition' image: ruby:2.1 services: - postgres @@ -1081,7 +1084,7 @@ test:mysql: - ruby ``` -You can see that the hidden jobs are conveniently used as templates. +You can see that the hidden keys are conveniently used as templates. ## Validate the .gitlab-ci.yml From f4f191c1a07c5b1616d81e0284f73f2a045e0783 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Aug 2016 09:36:46 +0200 Subject: [PATCH 115/141] Add Changelog entry for hidden keys fix in CI config --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index f4c850fe00c..8075e5b4d1d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,7 @@ v 8.12.0 (unreleased) - Adds response mime type to transaction metric action when it's not HTML v 8.11.3 (unreleased) + - Do not enforce using hash with hidden key in CI configuration. !6079 - Allow system info page to handle case where info is unavailable - Label list shows all issues (opened or closed) with that label - Don't show resolve conflicts link before MR status is updated From 2b33b24a3a5174cb391bf6c93643838f743e3dd2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 27 Aug 2016 12:36:08 -0500 Subject: [PATCH 116/141] Shorten task status phrase --- CHANGELOG | 1 + app/models/concerns/taskable.rb | 4 +- spec/features/task_lists_spec.rb | 274 ++++++++++++++++------- spec/support/taskable_shared_examples.rb | 71 ++++-- 4 files changed, 249 insertions(+), 101 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3548115dff3..ea673da5d3b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.12.0 (unreleased) - Reduce contributions calendar data payload (ClemMakesApps) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) + - Shorten task status phrase (ClemMakesApps) - Add hover color to emoji icon (ClemMakesApps) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Add `wiki_page_events` to project hook APIs (Ben Boeckel) diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index df2a9e3e84b..a3ac577cf3e 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -52,11 +52,11 @@ module Taskable end # Return a string that describes the current state of this Taskable's task - # list items, e.g. "20 tasks (12 completed, 8 remaining)" + # list items, e.g. "12 of 20 tasks completed" def task_status return '' if description.blank? sum = tasks.summary - "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)" + "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed" end end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 6ed279ef9be..abb27c90e0a 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do MARKDOWN end + let(:singleIncompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [ ] Incomplete entry 1 + MARKDOWN + end + + let(:singleCompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [x] Incomplete entry 1 + MARKDOWN + end + before do Warden.test_mode! @@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do end describe 'for Issues' do - let!(:issue) { create(:issue, description: markdown, author: user, project: project) } + describe 'multiple tasks' do + let!(:issue) { create(:issue, description: markdown, author: user, project: project) } - it 'renders' do - visit_issue(project, issue) + it 'renders' do + visit_issue(project, issue) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + container = '.detail-page-description .description.js-task-list-container' + + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end + + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end end - it 'contains the required selectors' do - visit_issue(project, issue) + describe 'single incomplete task' do + let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) } - container = '.detail-page-description .description.js-task-list-container' + it 'renders' do + visit_issue(project, issue) - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') + describe 'single complete task' do + let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) } - logout(:user) + it 'renders' do + visit_issue(project, issue) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end - it 'provides a summary on Issues#index' do - visit namespace_project_issues_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end describe 'for Notes' do let!(:issue) { create(:issue, author: user, project: project) } - let!(:note) do - create(:note, note: markdown, noteable: issue, - project: project, author: user) + describe 'multiple tasks' do + let!(:note) do + create(:note, note: markdown, noteable: issue, + project: project, author: user) + end + + it 'renders for note body' do + visit_issue(project, issue) + + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 6) + expect(page).to have_selector('.note ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + expect(page).to have_selector('.note .js-task-list-container') + expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') + expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + end + + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end end - it 'renders for note body' do - visit_issue(project, issue) + describe 'single incomplete task' do + let!(:note) do + create(:note, note: singleIncompleteMarkdown, noteable: issue, + project: project, author: user) + end - expect(page).to have_selector('.note ul.task-list', count: 1) - expect(page).to have_selector('.note li.task-list-item', count: 6) - expect(page).to have_selector('.note ul input[checked]', count: 2) + it 'renders for note body' do + visit_issue(project, issue) + + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 0) + end end - it 'contains the required selectors' do - visit_issue(project, issue) + describe 'single complete task' do + let!(:note) do + create(:note, note: singleCompleteMarkdown, noteable: issue, + project: project, author: user) + end - expect(page).to have_selector('.note .js-task-list-container') - expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') - expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') - end + it 'renders for note body' do + visit_issue(project, issue) - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') - - logout(:user) - - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 1) + end end end @@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do visit namespace_project_merge_request_path(project.namespace, project, merge) end - let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } + describe 'multiple tasks' do + let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } - it 'renders for description' do - visit_merge_request(project, merge) + it 'renders for description' do + visit_merge_request(project, merge) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_merge_request(project, merge) + + container = '.detail-page-description .description.js-task-list-container' + + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end + + it 'is only editable by author' do + visit_merge_request(project, merge) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end + end + + describe 'single incomplete task' do + let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'contains the required selectors' do - visit_merge_request(project, merge) + describe 'single complete task' do + let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) } - container = '.detail-page-description .description.js-task-list-container' + it 'renders for description' do + visit_merge_request(project, merge) - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end - it 'is only editable by author' do - visit_merge_request(project, merge) - expect(page).to have_selector('.js-task-list-container') - - logout(:user) - - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') - end - - it 'provides a summary on MergeRequests#index' do - visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end end diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index 927c72c7409..201614e45a4 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -3,30 +3,57 @@ # Requires a context containing: # subject { Issue or MergeRequest } shared_examples 'a Taskable' do - before do - subject.description = <<-EOT.strip_heredoc - * [ ] Task 1 - * [x] Task 2 - * [x] Task 3 - * [ ] Task 4 - * [ ] Task 5 - EOT - end - - it 'returns the correct task status' do - expect(subject.task_status).to match('5 tasks') - expect(subject.task_status).to match('2 completed') - expect(subject.task_status).to match('3 remaining') - end - - describe '#tasks?' do - it 'returns true when object has tasks' do - expect(subject.tasks?).to eq true + describe 'with multiple tasks' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + * [x] Task 2 + * [x] Task 3 + * [ ] Task 4 + * [ ] Task 5 + EOT end - it 'returns false when object has no tasks' do - subject.description = 'Now I have no tasks' - expect(subject.tasks?).to eq false + it 'returns the correct task status' do + expect(subject.task_status).to match('2 of') + expect(subject.task_status).to match('5 tasks completed') + end + + describe '#tasks?' do + it 'returns true when object has tasks' do + expect(subject.tasks?).to eq true + end + + it 'returns false when object has no tasks' do + subject.description = 'Now I have no tasks' + expect(subject.tasks?).to eq false + end + end + end + + describe 'with an incomplete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('0 of') + expect(subject.task_status).to match('1 task completed') + end + end + + describe 'with a complete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [x] Task 1 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('1 of') + expect(subject.task_status).to match('1 task completed') end end end From 8581e152ef8fa27b6670760d39b7f06dab5f796b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 27 Jul 2016 13:46:46 -0500 Subject: [PATCH 117/141] Add last commit time to repo view --- CHANGELOG | 1 + app/assets/stylesheets/pages/tree.scss | 10 ++++++++++ app/views/projects/tree/_tree_content.html.haml | 11 ++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 83a5d1727f3..09692af3b9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.12.0 (unreleased) - Fix groups sort dropdown alignment (ClemMakesApps) - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps) - Fix markdown help references (ClemMakesApps) + - Add last commit time to repo view (ClemMakesApps) - Added tests for diff notes - Add delimiter to project stars and forks count (ClemMakesApps) - Fix badge count alignment (ClemMakesApps) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 9da40fe2b09..538f211c65b 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -11,6 +11,16 @@ } } + .last-commit { + max-width: 506px; + + .last-commit-content { + @include str-truncated; + width: calc(100% - 140px); + margin-left: 3px; + } + } + .tree-table { margin-bottom: 0; diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 558e6146ae9..ca5d2d7722a 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -5,16 +5,17 @@ %tr %th Name %th Last Update - %th.hidden-xs - .pull-left Last Commit - .last-commit.hidden-sm.pull-left -   + %th.hidden-xs.last-commit + Last Commit + .last-commit-content.hidden-sm %i.fa.fa-angle-right   %small.light = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" – - = truncate(@commit.title, length: 50) + = time_ago_with_tooltip(@commit.committed_date) + – + = @commit.full_title = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - if @path.present? From 2fe2f67da8c325309bd2a0aee06e0068ac7061c4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 29 Aug 2016 11:05:22 -0500 Subject: [PATCH 118/141] Fix inconsistent background color for filter input field --- CHANGELOG | 1 + app/assets/stylesheets/framework/nav.scss | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 83a5d1727f3..b661f929024 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.12.0 (unreleased) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention) - Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Remove Gitorious import + - Fix inconsistent background color for filter input field (ClemMakesApps) - Add Sentry logging to API calls - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) - Fix groups sort dropdown alignment (ClemMakesApps) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ef2fe844f94..ba0a167c419 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -211,12 +211,6 @@ } } - .project-filter-form { - input { - background-color: $background-color; - } - } - @media (max-width: $screen-xs-max) { padding-bottom: 0; width: 100%; From 5f7f98ff6a50c64a32fc2a91f59154112d57ecae Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 27 Aug 2016 12:50:55 -0500 Subject: [PATCH 119/141] Remove vendor prefixes for linear-gradient CSS --- CHANGELOG | 1 + app/assets/stylesheets/framework/nav.scss | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3548115dff3..f7f97e11646 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.12.0 (unreleased) - Add two-factor recovery endpoint to internal API !5510 + - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) - Add font color contrast to external label in admin area (ClemMakesApps) - Change merge_error column from string to text type - Reduce contributions calendar data payload (ClemMakesApps) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index cf7cf125504..f0e134ebc16 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -8,10 +8,7 @@ height: 30px; transition-duration: .3s; -webkit-transform: translateZ(0); - background: -webkit-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); - background: -o-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); - background: -moz-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); - background: linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%); + background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4)); &.scrolling { visibility: visible; From 43d50117187db1d8e034dbfc01e894a108f55369 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 25 Aug 2016 16:42:20 +0100 Subject: [PATCH 120/141] Fix diff comments on legacy MRs --- CHANGELOG | 3 ++ app/models/legacy_diff_note.rb | 4 +++ .../merge_requests/diff_notes_spec.rb | 31 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index df8dec7bdde..f8391e996fb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,9 @@ v 8.12.0 (unreleased) - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) - Adds response mime type to transaction metric action when it's not HTML +v 8.11.4 (unreleased) + - Fix diff commenting on merge requests created prior to 8.10 + v 8.11.3 (unreleased) - Allow system info page to handle case where info is unavailable - Label list shows all issues (opened or closed) with that label diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 40277a9b139..0e1649aafe5 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -53,6 +53,10 @@ class LegacyDiffNote < Note self.line_code end + def to_discussion + Discussion.new([self]) + end + # Check if this note is part of an "active" discussion # # This will always return true for anything except MergeRequest noteables, diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb index a818679a874..06fad1007e8 100644 --- a/spec/features/merge_requests/diff_notes_spec.rb +++ b/spec/features/merge_requests/diff_notes_spec.rb @@ -147,6 +147,37 @@ feature 'Diff notes', js: true, feature: true do end end + context 'when the MR only supports legacy diff notes' do + before do + @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') + end + + context 'with a new line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + + context 'with an old line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + end + + context 'with an unchanged line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + end + end + + context 'with a match line' do + it 'should not allow commenting' do + should_not_allow_commenting(find('.match', match: :first)) + end + end + end + def should_allow_commenting(line_holder, diff_side = nil) line = get_line_components(line_holder, diff_side) line[:content].hover From 1bda1e62def69bc0525a558f92acf182dc05fe8d Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 29 Aug 2016 12:43:09 +0100 Subject: [PATCH 121/141] Fix resolving conflicts on forks Forks may not be up-to-date with the target project, and so might not contain one of the parent refs in their repo. Fetch this if it isn't present. --- CHANGELOG | 3 + .../merge_requests/resolve_service.rb | 16 +++- .../merge_requests/resolve_service_spec.rb | 87 +++++++++++++++++++ spec/support/test_env.rb | 47 +++++----- 4 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 spec/services/merge_requests/resolve_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index d06fc24d40a..d95072d9952 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,9 @@ v 8.12.0 (unreleased) - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) - Adds response mime type to transaction metric action when it's not HTML +v 8.11.4 (unreleased) + - Fix resolving conflicts on forks + v 8.11.3 (unreleased) - Allow system info page to handle case where info is unavailable - Label list shows all issues (opened or closed) with that label diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index adc71b0c2bc..bd8b9f8cfd4 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,11 +1,14 @@ module MergeRequests class ResolveService < MergeRequests::BaseService - attr_accessor :conflicts, :rugged, :merge_index + attr_accessor :conflicts, :rugged, :merge_index, :merge_request def execute(merge_request) @conflicts = merge_request.conflicts @rugged = project.repository.rugged @merge_index = conflicts.merge_index + @merge_request = merge_request + + fetch_their_commit! conflicts.files.each do |file| write_resolved_file_to_index(file, params[:sections]) @@ -27,5 +30,16 @@ module MergeRequests merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) merge_index.conflict_remove(our_path) end + + # If their commit (in the target project) doesn't exist in the source project, it + # can't be a parent for the merge commit we're about to create. If that's the case, + # fetch the target branch ref into the source project so the commit exists in both. + # + def fetch_their_commit! + return if rugged.include?(conflicts.their_commit.oid) + + remote = rugged.remotes.create_anonymous(merge_request.target_project.repository.path_to_repo) + remote.fetch(merge_request.target_branch) + end end end diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb new file mode 100644 index 00000000000..d71932458fa --- /dev/null +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe MergeRequests::ResolveService do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:fork_project) do + create(:forked_project_with_submodules) do |fork_project| + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + end + end + + let(:merge_request) do + create(:merge_request, + source_branch: 'conflict-resolvable', source_project: project, + target_branch: 'conflict-start') + end + + let(:merge_request_from_fork) do + create(:merge_request, + source_branch: 'conflict-resolvable-fork', source_project: fork_project, + target_branch: 'conflict-start', target_project: project) + end + + describe '#execute' do + context 'with valid params' do + let(:params) do + { + sections: { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + }, + commit_message: 'This is a commit message!' + } + end + + context 'when the source and target project are the same' do + before do + MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + end + + it 'creates a commit with the message' do + expect(merge_request.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request.source_branch_head.parents.map(&:id)). + to eq(['1450cd639e0bc6721eb02800169e464f212cde06', + '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + end + end + + context 'when the source project is a fork and does not contain the HEAD of the target branch' do + let!(:target_head) do + project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false) + end + + before do + MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork) + end + + it 'creates a commit with the message' do + expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request_from_fork.source_branch_head.parents.map(&:id)). + to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', + target_head]) + end + end + end + + context 'when a resolution is missing' do + let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingResolution error' do + expect { service.execute(merge_request) }. + to raise_error(Gitlab::Conflict::File::MissingResolution) + end + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index c7a45fc4ff9..0097dbf8fad 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -6,7 +6,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'empty-branch' => '7efb185', - 'ends-with.json' => '98b0d8b3', + 'ends-with.json' => '98b0d8b', 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', @@ -37,9 +37,10 @@ module TestEnv # need to keep all the branches in sync. # We currently only need a subset of the branches FORKED_BRANCH_SHA = { - 'add-submodule-version-bump' => '3f547c08', - 'master' => '5937ac0', - 'remove-submodule' => '2a33e0c0' + 'add-submodule-version-bump' => '3f547c0', + 'master' => '5937ac0', + 'remove-submodule' => '2a33e0c', + 'conflict-resolvable-fork' => '404fa3f' } # Test environment @@ -117,22 +118,7 @@ module TestEnv system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path})) end - Dir.chdir(repo_path) do - branch_sha.each do |branch, sha| - # Try to reset without fetching to avoid using the network. - reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) - unless system(*reset) - if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) - unless system(*reset) - raise 'The fetched test seed '\ - 'does not contain the required revision.' - end - else - raise 'Could not fetch test seed repository.' - end - end - end - end + set_repo_refs(repo_path, branch_sha) # We must copy bare repositories because we will push to them. system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare})) @@ -144,6 +130,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, BRANCH_SHA) end def repos_path @@ -160,6 +147,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, FORKED_BRANCH_SHA) end # When no cached assets exist, manually hit the root path to create them @@ -209,4 +197,23 @@ module TestEnv def git_env { 'GIT_TEMPLATE_DIR' => '' } end + + def set_repo_refs(repo_path, branch_sha) + Dir.chdir(repo_path) do + branch_sha.each do |branch, sha| + # Try to reset without fetching to avoid using the network. + reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) + unless system(*reset) + if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) + unless system(*reset) + raise 'The fetched test seed '\ + 'does not contain the required revision.' + end + else + raise 'Could not fetch test seed repository.' + end + end + end + end + end end From c9c2503c5186a38302ed606f793b52ffa394f52c Mon Sep 17 00:00:00 2001 From: Katarzyna Kobierska Date: Tue, 26 Jul 2016 13:57:43 +0200 Subject: [PATCH 122/141] User can edit closed MR with deleted fork Add test for closed MR without fork Add view test visibility of Reopen and Close buttons Fix controller tests and validation method Fix missing space Remove unused variables from test closed_without_fork? method refactoring Add information about missing fork When closed MR without fork can't edit target branch Tests for closed MR edit view Fix indentation and rebase, refactoring --- CHANGELOG | 1 + app/helpers/merge_requests_helper.rb | 2 +- app/models/merge_request.rb | 33 +++++----- app/services/merge_requests/update_service.rb | 4 ++ .../merge_requests/show/_mr_title.html.haml | 4 ++ app/views/shared/issuable/_form.html.haml | 41 ++++++------ .../merge_requests_controller_spec.rb | 29 +++++++++ spec/models/merge_request_spec.rb | 64 +++++++++++++++++++ .../merge_requests/edit.html.haml_spec.rb | 41 ++++++++++++ .../merge_requests/show.html.haml_spec.rb | 40 ++++++++++++ 10 files changed, 223 insertions(+), 36 deletions(-) create mode 100644 spec/views/projects/merge_requests/edit.html.haml_spec.rb create mode 100644 spec/views/projects/merge_requests/show.html.haml_spec.rb diff --git a/CHANGELOG b/CHANGELOG index a6cd8f4c7e1..cc98863dac8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -223,6 +223,7 @@ v 8.10.6 - Restore "Largest repository" sort option on Admin > Projects page. !5797 - Fix privilege escalation via project export. - Require administrator privileges to perform a project import. + - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 v 8.10.5 - Add a data migration to fix some missing timestamps in the members table. !5670 diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index db6e731c744..a9e175c3f5c 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -98,6 +98,6 @@ module MergeRequestsHelper end def merge_request_button_visibility(merge_request, closed) - return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) + return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1d05e4a85d1..b41a1f0c547 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -91,13 +91,13 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: [:allow_broken, :importing?] + validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches, unless: [:allow_broken, :importing?] - validate :validate_fork + validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] + validate :validate_fork, unless: :closed_without_fork? scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } @@ -305,19 +305,22 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project + return true if target_project == source_project + return true unless fork_missing? - if target_project == source_project - true - else - # If source and target projects are different - # we should check if source project is actually a fork of target project - if source_project.forked_from?(target_project) - true - else - errors.add :validate_fork, - 'Source project is not a fork of target project' - end - end + errors.add :validate_fork, + 'Source project is not a fork of target project' + end + + def closed_without_fork? + closed? && fork_missing? + end + + def fork_missing? + return false unless for_fork? + return true unless source_project + + !source_project.forked_from?(target_project) end def ensure_merge_request_diff diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 30c5f24988c..398ec47f0ea 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,6 +11,10 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + if merge_request.closed_without_fork? + params.except!(:target_branch, :force_remove_source_branch) + end + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) update(merge_request) diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 098ce19da21..48016645019 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,3 +1,7 @@ +- if @merge_request.closed_without_fork? + .alert.alert-danger + %p Source project is not a fork of the target project + .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box{ class: status_box_class(@merge_request) } diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 22594b46443..75753a6b0af 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -135,28 +135,29 @@ = icon('question-circle') - if issuable.is_a?(MergeRequest) - %hr - - if @merge_request.new_record? + - unless @merge_request.closed_without_fork? + %hr + - if @merge_request.new_record? + .form-group + = f.label :source_branch, class: 'control-label' + .col-sm-10 + .issuable-form-select-holder + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) .form-group - = f.label :source_branch, class: 'control-label' + = f.label :target_branch, class: 'control-label' .col-sm-10 .issuable-form-select-holder - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) - .form-group - = f.label :target_branch, class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - - if @merge_request.new_record? -   - = link_to 'Change branches', mr_change_branches_path(@merge_request) - - if @merge_request.can_remove_source_branch?(current_user) - .form-group - .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[force_remove_source_branch]' do - = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? - Remove source branch when merge request is accepted. + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) + - if @merge_request.new_record? +   + = link_to 'Change branches', mr_change_branches_path(@merge_request) + - if @merge_request.can_remove_source_branch?(current_user) + .form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? + Remove source branch when merge request is accepted. - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} @@ -175,7 +176,7 @@ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else .pull-right - - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) + - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c64c2b075c5..f95c3fc771b 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -170,6 +170,35 @@ describe Projects::MergeRequestsController do expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) expect(merge_request.reload.closed?).to be_truthy end + + it 'allow to edit closed MR' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + title: 'New title' + } + + expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) + expect(merge_request.reload.title).to eq 'New title' + end + + it 'does not allow to update target branch closed MR' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + target_branch: 'new_branch' + } + + expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d67f71bbb9c..5fea6adf329 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -962,4 +962,68 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy end end + + describe "#fork_missing?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "when fork exists" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it { expect(merge_request.fork_missing?).to be_falsey } + end + + context "when source project is the target project" do + let(:merge_request) { create(:merge_request, source_project: project) } + + it { expect(merge_request.fork_missing?).to be_falsey } + end + + context "when fork does not exist" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it do + unlink_project.execute + merge_request.reload + + expect(merge_request.fork_missing?).to be_truthy + end + end + end + + describe "#closed_without_fork?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "closed MR" do + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project) + end + + it "has a fork" do + expect(closed_merge_request.closed_without_fork?).to be_falsey + end + + it "does not have a fork" do + unlink_project.execute + closed_merge_request.reload + + expect(closed_merge_request.closed_without_fork?).to be_truthy + end + end + end end diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb new file mode 100644 index 00000000000..d7a1a2447ea --- /dev/null +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'projects/merge_requests/edit.html.haml' do + include Devise::TestHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project, + author: user) + end + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + before do + assign(:project, project) + assign(:merge_request, closed_merge_request) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:current_user).and_return(User.find(closed_merge_request.author_id)) + end + + context 'when closed MR without fork' do + it "shows editable fields" do + unlink_project.execute + closed_merge_request.reload + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_css('label', text: "Title") + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_css('label', text: "Description") + expect(rendered).to have_css('label', text: "Assignee") + expect(rendered).to have_css('label', text: "Milestone") + expect(rendered).to have_css('label', text: "Labels") + expect(rendered).not_to have_css('label', text: "Target branch") + end + end +end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb new file mode 100644 index 00000000000..ed12b730eeb --- /dev/null +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'projects/merge_requests/show.html.haml' do + include Devise::TestHelpers + + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + source_branch: 'add-submodule-version-bump', + target_branch: 'master', target_project: project) + end + + before do + assign(:project, project) + assign(:merge_request, merge_request) + assign(:commits_count, 0) + + merge_request.close! + allow(view).to receive(:can?).and_return(true) + end + + context 'closed MR' do + it 'shows Reopen button' do + render + + expect(rendered).to have_css('a', visible: true, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + + it 'does not show Reopen button without fork' do + fork_project.destroy + render + + expect(rendered).to have_css('a', visible: false, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + end +end From 2e08f1156998e9cd40b5eba5762182b8cb006c57 Mon Sep 17 00:00:00 2001 From: Katarzyna Kobierska Date: Tue, 9 Aug 2016 15:43:15 +0200 Subject: [PATCH 123/141] Improve code --- app/views/shared/issuable/_form.html.haml | 41 +++++++++---------- spec/models/merge_request_spec.rb | 20 +++++++-- .../merge_requests/edit.html.haml_spec.rb | 30 ++++++++++---- .../merge_requests/show.html.haml_spec.rb | 13 +++--- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 75753a6b0af..c6b60f37f57 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -134,30 +134,29 @@ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } = icon('question-circle') -- if issuable.is_a?(MergeRequest) - - unless @merge_request.closed_without_fork? - %hr - - if @merge_request.new_record? - .form-group - = f.label :source_branch, class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) +- if issuable.is_a?(MergeRequest) && !@merge_request.closed_without_fork? + %hr + - if @merge_request.new_record? .form-group - = f.label :target_branch, class: 'control-label' + = f.label :source_branch, class: 'control-label' .col-sm-10 .issuable-form-select-holder - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - - if @merge_request.new_record? -   - = link_to 'Change branches', mr_change_branches_path(@merge_request) - - if @merge_request.can_remove_source_branch?(current_user) - .form-group - .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[force_remove_source_branch]' do - = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? - Remove source branch when merge request is accepted. + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + .form-group + = f.label :target_branch, class: 'control-label' + .col-sm-10 + .issuable-form-select-holder + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) + - if @merge_request.new_record? +   + = link_to 'Change branches', mr_change_branches_path(@merge_request) + - if @merge_request.can_remove_source_branch?(current_user) + .form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? + Remove source branch when merge request is accepted. - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 5fea6adf329..17337833596 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -992,7 +992,7 @@ describe MergeRequest, models: true do target_project: project) end - it do + it "returns true" do unlink_project.execute merge_request.reload @@ -1007,23 +1007,35 @@ describe MergeRequest, models: true do let(:user) { create(:user) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } - context "closed MR" do + context "when merge request is closed" do let(:closed_merge_request) do create(:closed_merge_request, source_project: fork_project, target_project: project) end - it "has a fork" do + it "returns false if fork exist" do expect(closed_merge_request.closed_without_fork?).to be_falsey end - it "does not have a fork" do + it "returns true if fork doesn't exist" do unlink_project.execute closed_merge_request.reload expect(closed_merge_request.closed_without_fork?).to be_truthy end end + + context "when merge request is open" do + let(:open_merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns false" do + expect(open_merge_request.closed_without_fork?).to be_falsey + end + end end end diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index d7a1a2447ea..6fd108c5bae 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -6,36 +6,48 @@ describe 'projects/merge_requests/edit.html.haml' do let(:user) { create(:user) } let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:closed_merge_request) do create(:closed_merge_request, source_project: fork_project, target_project: project, author: user) end - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } before do assign(:project, project) assign(:merge_request, closed_merge_request) allow(view).to receive(:can?).and_return(true) - allow(view).to receive(:current_user).and_return(User.find(closed_merge_request.author_id)) + allow(view).to receive(:current_user) + .and_return(User.find(closed_merge_request.author_id)) end - context 'when closed MR without fork' do + context 'when closed merge request without fork' do it "shows editable fields" do unlink_project.execute closed_merge_request.reload + render expect(rendered).to have_field('merge_request[title]') - expect(rendered).to have_css('label', text: "Title") expect(rendered).to have_field('merge_request[description]') - expect(rendered).to have_css('label', text: "Description") - expect(rendered).to have_css('label', text: "Assignee") - expect(rendered).to have_css('label', text: "Milestone") - expect(rendered).to have_css('label', text: "Labels") - expect(rendered).not_to have_css('label', text: "Target branch") + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false) + end + end + + context 'when closed merge request with fork' do + it "shows editable fields" do + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).to have_selector('#merge_request_target_branch', visible: false) end end end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index ed12b730eeb..923c3553814 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -5,23 +5,24 @@ describe 'projects/merge_requests/show.html.haml' do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } - let(:merge_request) do - create(:merge_request, + + let(:closed_merge_request) do + create(:closed_merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', - target_branch: 'master', target_project: project) + target_branch: 'master', + target_project: project) end before do assign(:project, project) - assign(:merge_request, merge_request) + assign(:merge_request, closed_merge_request) assign(:commits_count, 0) - merge_request.close! allow(view).to receive(:can?).and_return(true) end - context 'closed MR' do + context 'when merge request is closed' do it 'shows Reopen button' do render From 6b02c82cfe68dc0f19cb3523eed1769a1e6d64b9 Mon Sep 17 00:00:00 2001 From: Katarzyna Kobierska Date: Wed, 10 Aug 2016 15:36:30 +0200 Subject: [PATCH 124/141] Improve grammar --- .../merge_requests/show/_mr_title.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 2 +- .../projects/merge_requests_controller_spec.rb | 4 ++-- spec/models/merge_request_spec.rb | 14 +++++++------- .../projects/merge_requests/edit.html.haml_spec.rb | 4 ++-- .../projects/merge_requests/show.html.haml_spec.rb | 7 ++++--- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 48016645019..e35291dff7d 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,6 +1,6 @@ - if @merge_request.closed_without_fork? .alert.alert-danger - %p Source project is not a fork of the target project + %p The source project of this merge request has been removed. .clearfix.detail-page-header .issuable-header diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c6b60f37f57..3856a4917b4 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -134,7 +134,7 @@ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } = icon('question-circle') -- if issuable.is_a?(MergeRequest) && !@merge_request.closed_without_fork? +- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork? %hr - if @merge_request.new_record? .form-group diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index f95c3fc771b..a219400d75f 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -171,7 +171,7 @@ describe Projects::MergeRequestsController do expect(merge_request.reload.closed?).to be_truthy end - it 'allow to edit closed MR' do + it 'allows editing of a closed merge request' do merge_request.close! put :update, @@ -186,7 +186,7 @@ describe Projects::MergeRequestsController do expect(merge_request.reload.title).to eq 'New title' end - it 'does not allow to update target branch closed MR' do + it 'does not allow to update target branch closed merge request' do merge_request.close! put :update, diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 17337833596..4cbf87ba792 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -969,7 +969,7 @@ describe MergeRequest, models: true do let(:user) { create(:user) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } - context "when fork exists" do + context "when the fork exists" do let(:merge_request) do create(:merge_request, source_project: fork_project, @@ -979,13 +979,13 @@ describe MergeRequest, models: true do it { expect(merge_request.fork_missing?).to be_falsey } end - context "when source project is the target project" do + context "when the source project is the same as the target project" do let(:merge_request) { create(:merge_request, source_project: project) } it { expect(merge_request.fork_missing?).to be_falsey } end - context "when fork does not exist" do + context "when the fork does not exist" do let(:merge_request) do create(:merge_request, source_project: fork_project, @@ -1007,18 +1007,18 @@ describe MergeRequest, models: true do let(:user) { create(:user) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } - context "when merge request is closed" do + context "when the merge request is closed" do let(:closed_merge_request) do create(:closed_merge_request, source_project: fork_project, target_project: project) end - it "returns false if fork exist" do + it "returns false if the fork exist" do expect(closed_merge_request.closed_without_fork?).to be_falsey end - it "returns true if fork doesn't exist" do + it "returns true if the fork does not exist" do unlink_project.execute closed_merge_request.reload @@ -1026,7 +1026,7 @@ describe MergeRequest, models: true do end end - context "when merge request is open" do + context "when the merge request is open" do let(:open_merge_request) do create(:merge_request, source_project: fork_project, diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 6fd108c5bae..31bbb150698 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -24,7 +24,7 @@ describe 'projects/merge_requests/edit.html.haml' do .and_return(User.find(closed_merge_request.author_id)) end - context 'when closed merge request without fork' do + context 'when a merge request without fork' do it "shows editable fields" do unlink_project.execute closed_merge_request.reload @@ -39,7 +39,7 @@ describe 'projects/merge_requests/edit.html.haml' do end end - context 'when closed merge request with fork' do + context 'when a merge request with an existing source project is closed' do it "shows editable fields" do render diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 923c3553814..02fe04253db 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -22,16 +22,17 @@ describe 'projects/merge_requests/show.html.haml' do allow(view).to receive(:can?).and_return(true) end - context 'when merge request is closed' do - it 'shows Reopen button' do + context 'when the merge request is closed' do + it 'shows the "Reopen" button' do render expect(rendered).to have_css('a', visible: true, text: 'Reopen') expect(rendered).to have_css('a', visible: false, text: 'Close') end - it 'does not show Reopen button without fork' do + it 'does not show the "Reopen" button when the source project does not exist' do fork_project.destroy + render expect(rendered).to have_css('a', visible: false, text: 'Reopen') From 8ed6e2ec7ad992dda45042bcacea9e00a1bc6ab5 Mon Sep 17 00:00:00 2001 From: Katarzyna Kobierska Date: Wed, 17 Aug 2016 13:12:13 +0200 Subject: [PATCH 125/141] Fix test --- .../projects/merge_requests/show.html.haml_spec.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 02fe04253db..fe0780e72df 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -3,15 +3,16 @@ require 'spec_helper' describe 'projects/merge_requests/show.html.haml' do include Devise::TestHelpers + let(:user) { create(:user) } let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } let(:closed_merge_request) do create(:closed_merge_request, source_project: fork_project, - source_branch: 'add-submodule-version-bump', - target_branch: 'master', - target_project: project) + target_project: project, + author: user) end before do @@ -31,7 +32,8 @@ describe 'projects/merge_requests/show.html.haml' do end it 'does not show the "Reopen" button when the source project does not exist' do - fork_project.destroy + unlink_project.execute + closed_merge_request.reload render From 4f8a823e64f9ba3e2c8ef4da8dddaab7b6f7fc3d Mon Sep 17 00:00:00 2001 From: Katarzyna Kobierska Date: Thu, 18 Aug 2016 07:14:44 +0000 Subject: [PATCH 126/141] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index cc98863dac8..1979158d439 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -217,13 +217,13 @@ v 8.11.0 v 8.10.7 - Upgrade Hamlit to 2.6.1. !5873 - Upgrade Doorkeeper to 4.2.0. !5881 + - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 v 8.10.6 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 - Restore "Largest repository" sort option on Admin > Projects page. !5797 - Fix privilege escalation via project export. - Require administrator privileges to perform a project import. - - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 v 8.10.5 - Add a data migration to fix some missing timestamps in the members table. !5670 From 7226631102ef00c2d880bc6c1e099e52f4fa8659 Mon Sep 17 00:00:00 2001 From: Katarzyna Kobierska Date: Thu, 25 Aug 2016 15:08:31 +0200 Subject: [PATCH 127/141] Improve grammar and fix CHANGELOG --- CHANGELOG | 8 +++++++- app/models/merge_request.rb | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1979158d439..c5d035661b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,13 @@ v 8.11.2 - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978 - Use gitlab-workhorse 0.7.11 !5983 - Does not halt the GitHub import process when an error occurs. !5763 + - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 + +v 8.11.2 (unreleased) + - Show "Create Merge Request" widget for push events to fork projects on the source project + +v 8.11.1 (unreleased) + - Does not halt the GitHub import process when an error occurs - Fix file links on project page when default view is Files !5933 - Fixed enter key in search input not working !5888 @@ -217,7 +224,6 @@ v 8.11.0 v 8.10.7 - Upgrade Hamlit to 2.6.1. !5873 - Upgrade Doorkeeper to 4.2.0. !5881 - - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 v 8.10.6 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b41a1f0c547..27ca5d119d5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -309,7 +309,7 @@ class MergeRequest < ActiveRecord::Base return true unless fork_missing? errors.add :validate_fork, - 'Source project is not a fork of target project' + 'Source project is not a fork of the target project' end def closed_without_fork? From 475afd37b656fa87e8f528670091a1923e391cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Doursenaud?= Date: Thu, 30 Jul 2015 12:38:21 +0000 Subject: [PATCH 128/141] Updated Bitbucket OmniAuth documentation --- doc/integration/bitbucket.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 2eb6266ebe7..0078f4e15b2 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -7,7 +7,7 @@ Bitbucket will generate an application ID and secret key for you to use. 1. Sign in to Bitbucket. -1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you. +1. Navigate to your individual user settings (Manage account) or a team's settings (Manage team), depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you. 1. Select "OAuth" in the left menu. @@ -16,9 +16,17 @@ Bitbucket will generate an application ID and secret key for you to use. 1. Provide the required details. - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Application description: Fill this in if you wish. + - Callback URL: leave blank. - URL: The URL to your GitLab installation. 'https://gitlab.company.com' + +1. Grant at least the following permissions. + - Account: Email + - Repositories: Read + 1. Select "Save". +1. Select your newly created OAuth consumer. + 1. You should now see a Key and Secret in the list of OAuth customers. Keep this page open as you continue configuration. @@ -62,7 +70,7 @@ Bitbucket will generate an application ID and secret key for you to use. app_secret: 'YOUR_APP_SECRET' } ``` -1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7. +1. Change 'YOUR_KEY' to the key from the Bitbucket application page from step 7. 1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7. @@ -137,4 +145,4 @@ To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org 1. Restart GitLab to allow it to find the new public key. -You should now see the "Import projects from Bitbucket" option on the New Project page enabled. +You should now see the "Import projects from Bitbucket" option on the New Project page enabled. \ No newline at end of file From c6d27652923ca287ab1ef29de3e2f4ab9117b121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Doursenaud?= Date: Thu, 30 Jul 2015 12:55:25 +0000 Subject: [PATCH 129/141] Updated Bitbucket OmniAuth documentation for omnibus package --- doc/integration/bitbucket.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 0078f4e15b2..94c845c29a4 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -92,7 +92,7 @@ Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and ### Step 1: Public key -To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. +To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/var/opt/gitlab/.ssh/bitbucket_rsa` for omnibus package and to `/home/git/.ssh/bitbucket_rsa.pub` for installations from source. If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: @@ -102,12 +102,20 @@ If you have that file in place, you're all set and should see the "Import projec sudo -u git -H ssh-keygen ``` - When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. + When asked `Enter file in which to save the key` specify the correct path, eg. `/var/opt/gitlab/.ssh/bitbucket_rsa` or `/home/git/.ssh/bitbucket_rsa`. Make sure to use an **empty passphrase**. 1. Configure SSH client to use your new key: Open the SSH configuration file of the git user. + + For omnibus package: + + ```sh + sudo editor /var/opt/gitlab/.ssh/config + ``` + + For installations from source: ```sh sudo editor /home/git/.ssh/config From 2d8d94a788eb0bf3885ee67bda9638556425fa4b Mon Sep 17 00:00:00 2001 From: Katarzyna Kobierska Date: Tue, 30 Aug 2016 13:31:39 +0200 Subject: [PATCH 130/141] Change method name --- CHANGELOG | 8 +------- app/models/merge_request.rb | 6 +++--- spec/models/merge_request_spec.rb | 8 ++++---- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c5d035661b1..5332aaa1ab2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -40,6 +40,7 @@ v 8.12.0 (unreleased) - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) - Adds response mime type to transaction metric action when it's not HTML - Fix hover leading space bug in pipeline graph !5980 + - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 v 8.11.3 (unreleased) - Allow system info page to handle case where info is unavailable @@ -53,13 +54,6 @@ v 8.11.2 - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978 - Use gitlab-workhorse 0.7.11 !5983 - Does not halt the GitHub import process when an error occurs. !5763 - - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 - -v 8.11.2 (unreleased) - - Show "Create Merge Request" widget for push events to fork projects on the source project - -v 8.11.1 (unreleased) - - Does not halt the GitHub import process when an error occurs - Fix file links on project page when default view is Files !5933 - Fixed enter key in search input not working !5888 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 27ca5d119d5..a8dd4a306cf 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -306,17 +306,17 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project return true if target_project == source_project - return true unless fork_missing? + return true unless forked_source_project_missing? errors.add :validate_fork, 'Source project is not a fork of the target project' end def closed_without_fork? - closed? && fork_missing? + closed? && forked_source_project_missing? end - def fork_missing? + def forked_source_project_missing? return false unless for_fork? return true unless source_project diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 4cbf87ba792..901b7bad007 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -963,7 +963,7 @@ describe MergeRequest, models: true do end end - describe "#fork_missing?" do + describe "#forked_source_project_missing?" do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } let(:user) { create(:user) } @@ -976,13 +976,13 @@ describe MergeRequest, models: true do target_project: project) end - it { expect(merge_request.fork_missing?).to be_falsey } + it { expect(merge_request.forked_source_project_missing?).to be_falsey } end context "when the source project is the same as the target project" do let(:merge_request) { create(:merge_request, source_project: project) } - it { expect(merge_request.fork_missing?).to be_falsey } + it { expect(merge_request.forked_source_project_missing?).to be_falsey } end context "when the fork does not exist" do @@ -996,7 +996,7 @@ describe MergeRequest, models: true do unlink_project.execute merge_request.reload - expect(merge_request.fork_missing?).to be_truthy + expect(merge_request.forked_source_project_missing?).to be_truthy end end end From 2fb28dddfc7848a90294c2008b5d672a305a8596 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 30 Aug 2016 15:42:40 +0200 Subject: [PATCH 131/141] Refactor Bitbucket integration documentation --- doc/integration/bitbucket.md | 194 +++++++++++++++++++++-------------- doc/integration/omniauth.md | 4 +- 2 files changed, 121 insertions(+), 77 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 94c845c29a4..16e54102113 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -1,127 +1,155 @@ -# Integrate your server with Bitbucket +# Integrate your GitLab server with Bitbucket -Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account. +Import projects from Bitbucket and login to your GitLab instance with your +Bitbucket account. -To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket. -Bitbucket will generate an application ID and secret key for you to use. +## Overview + +You can set up Bitbucket as an OAuth provider so that you can use your +credentials to authenticate into GitLab or import your projects from Bitbucket. + +- To use Bitbucket as an OmniAuth provider, follow the [Bitbucket OmniAuth + provider](#bitbucket-omniauth-provider) section. +- To import projects from Bitbucket, follow both the + [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and + [Bitbucket project import](#bitbucket-project-import) sections. + +## Bitbucket OmniAuth provider + +> **Note:** +Make sure to first follow the [Initial OmniAuth configuration][init-oauth] +before proceeding with setting up the Bitbucket integration. + +To enable the Bitbucket OmniAuth provider you must register your application +with Bitbucket. Bitbucket will generate an application ID and secret key for +you to use. 1. Sign in to Bitbucket. - -1. Navigate to your individual user settings (Manage account) or a team's settings (Manage team), depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you. - +1. Navigate to your individual user settings (Manage account) or a team's + settings (Manage team), depending on how you want the application registered. + It does not matter if the application is registered as an individual or a + team - that is entirely up to you. 1. Select "OAuth" in the left menu. - 1. Select "Add consumer". +1. Provide the required details: -1. Provide the required details. - - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - - Application description: Fill this in if you wish. - - Callback URL: leave blank. - - URL: The URL to your GitLab installation. 'https://gitlab.company.com' + | Item | Description | + | :--- | :---------- | + | **Name** | This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. | + | **Application description** | Fill this in if you wish. | + | **Callback URL** | Leave blank. | + | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | + +1. Grant at least the following permissions: + + ``` + Account: Email + Repositories: Read + ``` -1. Grant at least the following permissions. - - Account: Email - - Repositories: Read - 1. Select "Save". - 1. Select your newly created OAuth consumer. - 1. You should now see a Key and Secret in the list of OAuth customers. Keep this page open as you continue configuration. - 1. On your GitLab server, open the configuration file. For omnibus package: ```sh - sudo editor /etc/gitlab/gitlab.rb + sudo editor /etc/gitlab/gitlab.rb ``` For installations from source: ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` 1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. - 1. Add the provider configuration: For omnibus package: ```ruby - gitlab_rails['omniauth_providers'] = [ - { - "name" => "bitbucket", - "app_id" => "YOUR_KEY", - "app_secret" => "YOUR_APP_SECRET", - "url" => "https://bitbucket.org/" - } - ] + gitlab_rails['omniauth_providers'] = [ + { + "name" => "bitbucket", + "app_id" => "YOUR_KEY", + "app_secret" => "YOUR_APP_SECRET", + "url" => "https://bitbucket.org/" + } + ] ``` For installation from source: - ``` - - { name: 'bitbucket', app_id: 'YOUR_KEY', + ```yaml + - { name: 'bitbucket', + app_id: 'YOUR_KEY', app_secret: 'YOUR_APP_SECRET' } ``` 1. Change 'YOUR_KEY' to the key from the Bitbucket application page from step 7. - 1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7. - 1. Save the configuration file. - -1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```). - 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a Bitbucket icon below the regular sign in form. -Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. -If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a Bitbucket icon below the regular sign +in form. Click the icon to begin the authentication process. Bitbucket will ask +the user to sign in and authorize the GitLab application. If everything goes +well the user will be returned to GitLab and will be signed in. ## Bitbucket project import -To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com. +To allow projects to be imported directly into GitLab, Bitbucket requires two +extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md). -Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. +Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and +instead requires GitLab to use SSH and identify itself using your GitLab +server's SSH key. -### Step 1: Public key +To be able to access repositories on Bitbucket, GitLab will automatically +register your public key with Bitbucket as a deploy key for the repositories to +be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which +translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to +`/home/git/.ssh/bitbucket_rsa.pub` for installations from source. -To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/var/opt/gitlab/.ssh/bitbucket_rsa` for omnibus package and to `/home/git/.ssh/bitbucket_rsa.pub` for installations from source. +--- -If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: +Below are the steps that will allow GitLab to be able to import your projects +from Bitbucket. -1. Create a new SSH key: +1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider). +1. Create a new SSH key with an **empty passphrase**: ```sh sudo -u git -H ssh-keygen ``` - When asked `Enter file in which to save the key` specify the correct path, eg. `/var/opt/gitlab/.ssh/bitbucket_rsa` or `/home/git/.ssh/bitbucket_rsa`. - Make sure to use an **empty passphrase**. + When asked to 'Enter file in which to save the key' enter: + `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or + `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is + important so make sure to get it right. -1. Configure SSH client to use your new key: + > **Warning:** + This key must NOT be associated with ANY existing Bitbucket accounts. If it + is, the import will fail with an `Access denied! Please verify you can add + deploy keys to this repository.` error. + +1. Next, you need to to configure the SSH client to use your new key. Open the + SSH configuration file of the `git` user: - Open the SSH configuration file of the git user. - - For omnibus package: - - ```sh - sudo editor /var/opt/gitlab/.ssh/config ``` - - For installations from source: + # For Omnibus packages + sudo editor /var/opt/gitlab/.ssh/config - ```sh - sudo editor /home/git/.ssh/config + # For installations from source + sudo editor /home/git/.ssh/config ``` - Add a host configuration for `bitbucket.org`. +1. Add a host configuration for `bitbucket.org`: ```sh Host bitbucket.org @@ -129,28 +157,44 @@ If you have that file in place, you're all set and should see the "Import projec User git ``` -### Step 2: Known hosts - -To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: - -1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: +1. Save the file and exit. +1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git` + user that GitLab will use: ```sh sudo -u git -H ssh bitbucket.org ``` -1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): + That step is performed because GitLab needs to connect to Bitbucket over SSH, + in order to add `bitbucket.org` to your GitLab server's known SSH hosts. + +1. Verify the RSA key fingerprint you'll see in the response matches the one + in the [Bitbucket documentation][bitbucket-docs] (the specific IP address + doesn't matter): ```sh - The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. - RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. + The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established. + RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A. Are you sure you want to continue connecting (yes/no)? ``` -1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. - -1. Your GitLab server is now able to connect to Bitbucket over SSH. - +1. If the fingerprint matches, type `yes` to continue connecting and have + `bitbucket.org` be added to your known SSH hosts. After confirming you should + see a permission denied message. If you see an authentication successful + message you have done something wrong. The key you are using has already been + added to a Bitbucket account and will cause the import script to fail. Ensure + the key you are using CANNOT authenticate with Bitbucket. 1. Restart GitLab to allow it to find the new public key. -You should now see the "Import projects from Bitbucket" option on the New Project page enabled. \ No newline at end of file +Your GitLab server is now able to connect to Bitbucket over SSH. You should be +able to see the "Import projects from Bitbucket" option on the New Project page +enabled. + +## Acknowledgemts + +Special thanks to the writer behind the following article: + +- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/ + +[init-oauth]: omniauth.md#initial-omniauth-configuration +[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 46b260e7033..8a55fce96fe 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -102,8 +102,8 @@ To change these settings: block_auto_created_users: true ``` -Now we can choose one or more of the Supported Providers listed above to continue -the configuration process. +Now we can choose one or more of the [Supported Providers](#supported-providers) +listed above to continue the configuration process. ## Enable OmniAuth for an Existing User From 59dd9e576bd62f9311316ca31ecaba8ddde50b00 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 30 Aug 2016 16:23:45 +0100 Subject: [PATCH 132/141] Use Repository#fetch_ref --- app/services/merge_requests/resolve_service.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index bd8b9f8cfd4..19caa038c44 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -38,8 +38,13 @@ module MergeRequests def fetch_their_commit! return if rugged.include?(conflicts.their_commit.oid) - remote = rugged.remotes.create_anonymous(merge_request.target_project.repository.path_to_repo) - remote.fetch(merge_request.target_branch) + random_string = SecureRandom.hex + + project.repository.fetch_ref( + merge_request.target_project.repository.path_to_repo, + "refs/heads/#{merge_request.target_branch}", + "refs/tmp/#{random_string}/head" + ) end end end From 9b57ad382e69044eb851f64cc0eb35896baa712a Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 30 Aug 2016 16:30:42 +0100 Subject: [PATCH 133/141] Move #to_discussion to NoteOnDiff --- app/models/concerns/note_on_diff.rb | 4 ++++ app/models/diff_note.rb | 4 ---- app/models/legacy_diff_note.rb | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index a881fb83b7f..b8dd27a7afe 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -28,4 +28,8 @@ module NoteOnDiff def can_be_award_emoji? false end + + def to_discussion + Discussion.new([self]) + end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index c8320ff87fa..4442cefc7e9 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -107,10 +107,6 @@ class DiffNote < Note self.noteable.find_diff_discussion(self.discussion_id) end - def to_discussion - Discussion.new([self]) - end - private def supported? diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 0e1649aafe5..40277a9b139 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -53,10 +53,6 @@ class LegacyDiffNote < Note self.line_code end - def to_discussion - Discussion.new([self]) - end - # Check if this note is part of an "active" discussion # # This will always return true for anything except MergeRequest noteables, From 8fe7817e4d1ec0d97a3d924e2263c9de939efa92 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 30 Aug 2016 17:52:14 +0200 Subject: [PATCH 134/141] More Bitbucket integration refactoring --- doc/integration/bitbucket.md | 93 ++++++++++-------- doc/integration/img/bitbucket_oauth_keys.png | Bin 0 -> 12073 bytes .../img/bitbucket_oauth_settings_page.png | Bin 0 -> 82818 bytes 3 files changed, 52 insertions(+), 41 deletions(-) create mode 100644 doc/integration/img/bitbucket_oauth_keys.png create mode 100644 doc/integration/img/bitbucket_oauth_settings_page.png diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 16e54102113..556d71b8b76 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -1,14 +1,15 @@ # Integrate your GitLab server with Bitbucket -Import projects from Bitbucket and login to your GitLab instance with your -Bitbucket account. +Import projects from Bitbucket.org and login to your GitLab instance with your +Bitbucket.org account. ## Overview -You can set up Bitbucket as an OAuth provider so that you can use your -credentials to authenticate into GitLab or import your projects from Bitbucket. +You can set up Bitbucket.org as an OAuth provider so that you can use your +credentials to authenticate into GitLab or import your projects from +Bitbucket.org. -- To use Bitbucket as an OmniAuth provider, follow the [Bitbucket OmniAuth +- To use Bitbucket.org as an OmniAuth provider, follow the [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) section. - To import projects from Bitbucket, follow both the [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and @@ -21,16 +22,16 @@ Make sure to first follow the [Initial OmniAuth configuration][init-oauth] before proceeding with setting up the Bitbucket integration. To enable the Bitbucket OmniAuth provider you must register your application -with Bitbucket. Bitbucket will generate an application ID and secret key for +with Bitbucket.org. Bitbucket will generate an application ID and secret key for you to use. -1. Sign in to Bitbucket. -1. Navigate to your individual user settings (Manage account) or a team's - settings (Manage team), depending on how you want the application registered. +1. Sign in to [Bitbucket.org](https://bitbucket.org). +1. Navigate to your individual user settings (**Bitbucket settings**) or a team's + settings (**Manage team**), depending on how you want the application registered. It does not matter if the application is registered as an individual or a - team - that is entirely up to you. -1. Select "OAuth" in the left menu. -1. Select "Add consumer". + team, that is entirely up to you. +1. Select **OAuth** in the left menu under "Access Management". +1. Select **Add consumer**. 1. Provide the required details: | Item | Description | @@ -40,66 +41,74 @@ you to use. | **Callback URL** | Leave blank. | | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | -1. Grant at least the following permissions: + And grant at least the following permissions: ``` Account: Email - Repositories: Read + Repositories: Read, Admin ``` -1. Select "Save". -1. Select your newly created OAuth consumer. -1. You should now see a Key and Secret in the list of OAuth customers. - Keep this page open as you continue configuration. -1. On your GitLab server, open the configuration file. + >**Note:** + It may seem a little odd to giving GitLab admin permissions to repositories, + but this is needed in order for GitLab to be able to clone the repositories. - For omnibus package: + ![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png) - ```sh +1. Select **Save**. +1. Select your newly created OAuth consumer and you should now see a Key and + Secret in the list of OAuth customers. Keep this page open as you continue + the configuration. + + ![Bitbucket OAuth key](img/bitbucket_oauth_keys.png) + +1. On your GitLab server, open the configuration file: + + ``` + # For Omnibus packages sudo editor /etc/gitlab/gitlab.rb + + # For installations from source + sudo -u git -H editor /home/git/gitlab/config/gitlab.yml ``` - For installations from source: +1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) + for initial settings. +1. Add the Bitbucket provider configuration: - ```sh - cd /home/git/gitlab - - sudo -u git -H editor config/gitlab.yml - ``` - -1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. -1. Add the provider configuration: - - For omnibus package: + For Omnibus packages: ```ruby gitlab_rails['omniauth_providers'] = [ { "name" => "bitbucket", - "app_id" => "YOUR_KEY", - "app_secret" => "YOUR_APP_SECRET", + "app_id" => "BITBUCKET_APP_KEY", + "app_secret" => "BITBUCKET_APP_SECRET", "url" => "https://bitbucket.org/" } ] ``` - For installation from source: + For installations from source: ```yaml - { name: 'bitbucket', - app_id: 'YOUR_KEY', - app_secret: 'YOUR_APP_SECRET' } + app_id: 'BITBUCKET_APP_KEY', + app_secret: 'BITBUCKET_APP_SECRET' } ``` -1. Change 'YOUR_KEY' to the key from the Bitbucket application page from step 7. -1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7. + --- + + Where `BITBUCKET_APP_KEY` is the Key and `BITBUCKET_APP_SECRET` the Secret + from the Bitbucket application page. + 1. Save the configuration file. -1. Restart GitLab for the changes to take effect. +1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you + installed GitLab via Omnibus or from source respectively. On the sign in page there should now be a Bitbucket icon below the regular sign in form. Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. If everything goes -well the user will be returned to GitLab and will be signed in. +well, the user will be returned to GitLab and will be signed in. ## Bitbucket project import @@ -198,3 +207,5 @@ Special thanks to the writer behind the following article: [init-oauth]: omniauth.md#initial-omniauth-configuration [bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb2f7524a3e24b4727cb4b888a8f884116e7534 GIT binary patch literal 12073 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 bSNCW_@&}HE2p6CQ9;6_nB3&YB{ONxI^{%fM literal 0 HcmV?d00001 diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png new file mode 100644 index 0000000000000000000000000000000000000000..a3047712d8c1f670184e564c23329e2b653445be GIT binary patch literal 82818 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Ϊ`n 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 JxzFkRKLE#gUE2Tv literal 0 HcmV?d00001 From 47ca2d3202539f759552a277d4342591afd869a9 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 30 Aug 2016 11:55:57 -0500 Subject: [PATCH 135/141] Add new icon for created pipeline --- app/assets/stylesheets/pages/status.scss | 9 +++++++++ app/helpers/ci_status_helper.rb | 2 +- app/views/shared/icons/_icon_status_created.svg | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 app/views/shared/icons/_icon_status_created.svg diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 587f2d9f3c1..0ee7ceecae5 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -43,6 +43,15 @@ border-color: $blue-normal; } + &.ci-created { + color: $table-text-gray; + border-color: $table-text-gray; + + svg { + fill: $table-text-gray; + } + } + svg { height: 13px; width: 13px; diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 0327b476d18..00bdb488c9b 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -41,7 +41,7 @@ module CiStatusHelper when 'play' 'icon_play' when 'created' - 'icon_status_pending' + 'icon_status_created' else 'icon_status_cancel' end diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg new file mode 100644 index 00000000000..4a08fd65860 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created.svg @@ -0,0 +1 @@ + From 7532c012c26fc116f7c39f7c88ac3b08d818955c Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Mon, 8 Aug 2016 17:25:39 +0100 Subject: [PATCH 136/141] user is now notified when creating an issue through the api --- CHANGELOG | 1 + lib/api/issues.rb | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 03d6be67d6b..c440aa1987a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -70,6 +70,7 @@ v 8.11.0 - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) - Add Koding (online IDE) integration - Ability to specify branches for Pivotal Tracker integration (Egor Lynko) + - Creating an issue through our API now emails label subscribers !5720 - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres) - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres) - Fix adding line comments on the initial commit to a repo !5900 diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 077258faee1..1121285f0af 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -154,6 +154,20 @@ module API render_api_error!({ labels: errors }, 400) end + if params[:labels].present? + params[:labels] = params[:labels].split(",").each { |word| word.strip! } + attrs[:label_ids] = [] + + params[:labels].each do |label| + existing_label = user_project.labels.where(title: label).first + + unless existing_label.nil? + attrs[:label_ids] << existing_label.id + params[:labels].delete(label) + end + end + end + project = user_project issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute @@ -163,10 +177,10 @@ module API end if issue.valid? - # Find or create labels and attach to issue. Labels are valid because + # create new labels and attach to issue. Labels are valid because # we already checked its name, so there can't be an error here if params[:labels].present? - issue.add_labels_by_names(params[:labels].split(',')) + issue.add_labels_by_names(params[:labels]) end present issue, with: Entities::Issue, current_user: current_user From b7d29ce659412e9a2acc411c841420eb13d115ba Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Tue, 9 Aug 2016 23:08:59 +0100 Subject: [PATCH 137/141] adds test to check whether or not an email is sent to label subscribers after creating a new issue through the api --- lib/api/issues.rb | 25 ++++++------------------- spec/requests/api/issues_spec.rb | 13 +++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 1121285f0af..9a042e6e70d 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -154,35 +154,22 @@ module API render_api_error!({ labels: errors }, 400) end + # Find or create labels if params[:labels].present? - params[:labels] = params[:labels].split(",").each { |word| word.strip! } - attrs[:label_ids] = [] - - params[:labels].each do |label| - existing_label = user_project.labels.where(title: label).first - - unless existing_label.nil? - attrs[:label_ids] << existing_label.id - params[:labels].delete(label) - end + attrs[:label_ids] = params[:labels].split(",").map do |label_name| + user_project.labels.create_with(color: Label::DEFAULT_COLOR) + .find_or_create_by(title: label_name.strip) + .id end end - project = user_project - - issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute + issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end if issue.valid? - # create new labels and attach to issue. Labels are valid because - # we already checked its name, so there can't be an error here - if params[:labels].present? - issue.add_labels_by_names(params[:labels]) - end - present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index b8038fc85a1..a4c91252472 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:non_member) { create(:user) } @@ -478,6 +479,18 @@ describe API::API, api: true do expect(json_response['labels']).to eq(['label', 'label2']) end + it "emails label subscribers" do + clear_enqueued_jobs + label = project.labels.first + label.toggle_subscription(user2) + + expect do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: label.title + end.to change{enqueued_jobs.size}.by(1) + expect(response.status).to eq(201) + 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_http_status(400) From 7f0bcf04323ad69b64a90112896971ea8d1a5f99 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Mon, 15 Aug 2016 17:50:41 +0100 Subject: [PATCH 138/141] refactors update issue api request and some minor comments --- app/services/issuable_base_service.rb | 7 ++++++- lib/api/helpers.rb | 8 ++++++++ lib/api/issues.rb | 26 ++++++++++---------------- spec/requests/api/issues_spec.rb | 22 +++++++++++++++++----- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e06c37c323e..3b37365612e 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -162,7 +162,12 @@ class IssuableBaseService < BaseService if params.present? && update_issuable(issuable, params) issuable.reset_events_cache - handle_common_system_notes(issuable, old_labels: old_labels) + + # We do not touch as it will affect a update on updated_at field + ActiveRecord::Base.no_touching do + handle_common_system_notes(issuable, old_labels: old_labels) + end + handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index da4b1bf9902..dbad86d8926 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -102,6 +102,14 @@ module API label || not_found!('Label') end + def get_label_ids(labels) + labels.split(",").map do |label_name| + user_project.labels.create_with(color: Label::DEFAULT_COLOR) + .find_or_create_by(title: label_name.strip) + .id + end + end + def find_project_issue(id) issue = user_project.issues.find(id) not_found! unless can?(current_user, :read_issue, issue) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 9a042e6e70d..39a46f69f16 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -154,14 +154,9 @@ module API render_api_error!({ labels: errors }, 400) end - # Find or create labels - if params[:labels].present? - attrs[:label_ids] = params[:labels].split(",").map do |label_name| - user_project.labels.create_with(color: Label::DEFAULT_COLOR) - .find_or_create_by(title: label_name.strip) - .id - end - end + # Find or create labels to attach to the issue. Labels are vaild + # because we already checked its name, so there can't be an error here + attrs[:label_ids] = get_label_ids(params[:labels]) if params[:labels].present? issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute @@ -203,17 +198,16 @@ module API render_api_error!({ labels: errors }, 400) end + # Find or create labels and attach to issue. Labels are valid because + # we already checked its name, so there can't be an error here + if params[:labels] && can?(current_user, :admin_issue, user_project) + issue.remove_labels + attrs[:label_ids] = get_label_ids(params[:labels]) + end + issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) if issue.valid? - # Find or create labels and attach to issue. Labels are valid because - # we already checked its name, so there can't be an error here - if params[:labels] && can?(current_user, :admin_issue, user_project) - issue.remove_labels - # Create and add labels to the new created issue - issue.add_labels_by_names(params[:labels].split(',')) - end - present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index a4c91252472..009fb3b2d70 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -479,16 +479,16 @@ describe API::API, api: true do expect(json_response['labels']).to eq(['label', 'label2']) end - it "emails label subscribers" do - clear_enqueued_jobs + it "sends notifications for subscribers of newly added labels" do label = project.labels.first label.toggle_subscription(user2) - expect do + perform_enqueued_jobs do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: label.title - end.to change{enqueued_jobs.size}.by(1) - expect(response.status).to eq(201) + end + + should_email(user2) end it "returns a 400 bad request if title not given" do @@ -646,6 +646,18 @@ describe API::API, api: true 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 = project.labels.first + label.toggle_subscription(user2) + + perform_enqueued_jobs do + put 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 api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' From 76c2901eac89b1b3a9975ec0f91fb929fbed2e70 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Thu, 18 Aug 2016 11:24:44 +0100 Subject: [PATCH 139/141] if issue is not valid we revert back to the old labels when updating --- CHANGELOG | 2 +- app/services/issuable_base_service.rb | 13 +++++++++++++ lib/api/helpers.rb | 8 -------- lib/api/issues.rb | 11 ++--------- spec/requests/api/issues_spec.rb | 2 +- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c440aa1987a..313969a87e2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.12.0 (unreleased) v 8.11.4 (unreleased) - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner) + - Creating an issue through our API now emails label subscribers !5720 v 8.11.3 (unreleased) - Allow system info page to handle case where info is unavailable @@ -70,7 +71,6 @@ v 8.11.0 - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) - Add Koding (online IDE) integration - Ability to specify branches for Pivotal Tracker integration (Egor Lynko) - - Creating an issue through our API now emails label subscribers !5720 - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres) - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres) - Fix adding line comments on the initial commit to a repo !5900 diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 3b37365612e..4c8d93999a7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -45,6 +45,7 @@ class IssuableBaseService < BaseService unless can?(current_user, ability, project) params.delete(:milestone_id) + params.delete(:labels) params.delete(:add_label_ids) params.delete(:remove_label_ids) params.delete(:label_ids) @@ -72,6 +73,7 @@ class IssuableBaseService < BaseService filter_labels_in_param(:add_label_ids) filter_labels_in_param(:remove_label_ids) filter_labels_in_param(:label_ids) + find_or_create_label_ids end def filter_labels_in_param(key) @@ -80,6 +82,17 @@ class IssuableBaseService < BaseService params[key] = project.labels.where(id: params[key]).pluck(:id) end + def find_or_create_label_ids + labels = params.delete(:labels) + return unless labels + + params[:label_ids] = labels.split(",").map do |label_name| + project.labels.create_with(color: Label::DEFAULT_COLOR) + .find_or_create_by(title: label_name.strip) + .id + end + end + def process_label_ids(attributes, existing_label_ids: nil) label_ids = attributes.delete(:label_ids) add_label_ids = attributes.delete(:add_label_ids) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index dbad86d8926..da4b1bf9902 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -102,14 +102,6 @@ module API label || not_found!('Label') end - def get_label_ids(labels) - labels.split(",").map do |label_name| - user_project.labels.create_with(color: Label::DEFAULT_COLOR) - .find_or_create_by(title: label_name.strip) - .id - end - end - def find_project_issue(id) issue = user_project.issues.find(id) not_found! unless can?(current_user, :read_issue, issue) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 39a46f69f16..d0bc7243e54 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -154,9 +154,7 @@ module API render_api_error!({ labels: errors }, 400) end - # Find or create labels to attach to the issue. Labels are vaild - # because we already checked its name, so there can't be an error here - attrs[:label_ids] = get_label_ids(params[:labels]) if params[:labels].present? + attrs[:labels] = params[:labels] if params[:labels] issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute @@ -198,12 +196,7 @@ module API render_api_error!({ labels: errors }, 400) end - # Find or create labels and attach to issue. Labels are valid because - # we already checked its name, so there can't be an error here - if params[:labels] && can?(current_user, :admin_issue, user_project) - issue.remove_labels - attrs[:label_ids] = get_label_ids(params[:labels]) - end + attrs[:labels] = params[:labels] if params[:labels] issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 009fb3b2d70..3362a88d798 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -647,7 +647,7 @@ describe API::API, api: true do end it "sends notifications for subscribers of newly added labels when issue is updated" do - label = project.labels.first + label = create(:label, title: 'foo', color: '#FFAABB', project: project) label.toggle_subscription(user2) perform_enqueued_jobs do From a476e6f5e53c54bcc74a482f0695564713da7dd0 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 30 Aug 2016 22:44:11 +0300 Subject: [PATCH 140/141] Remove default value for lock_version --- ...7011312_ensure_lock_version_has_no_default.rb | 16 ++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160827011312_ensure_lock_version_has_no_default.rb diff --git a/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb new file mode 100644 index 00000000000..7c55bc23cf2 --- /dev/null +++ b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb @@ -0,0 +1,16 @@ +class EnsureLockVersionHasNoDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_default :issues, :lock_version, nil + change_column_default :merge_requests, :lock_version, nil + + execute('UPDATE issues SET lock_version = 1 WHERE lock_version = 0') + execute('UPDATE merge_requests SET lock_version = 1 WHERE lock_version = 0') + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 227e10294e4..0cd8648da2e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160824103857) do +ActiveRecord::Schema.define(version: 20160827011312) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 03d9e2458845c35f3912fca5b1585d1346920453 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 30 Aug 2016 14:26:14 -0700 Subject: [PATCH 141/141] Fix CHANGELOG Remove duplicate 8.11.4 entries and mark 8.11.3 as released. [ci skip] --- CHANGELOG | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 16fefd63d41..c07e194358d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,14 +51,10 @@ v 8.12.0 (unreleased) v 8.11.4 (unreleased) - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner) - Creating an issue through our API now emails label subscribers !5720 - -v 8.11.4 (unreleased) - Fix resolving conflicts on forks - -v 8.11.4 (unreleased) - Fix diff commenting on merge requests created prior to 8.10 -v 8.11.3 (unreleased) +v 8.11.3 - Do not enforce using hash with hidden key in CI configuration. !6079 - Allow system info page to handle case where info is unavailable - Label list shows all issues (opened or closed) with that label