From cf53d798736f7c86459c3e6635a83875e6101373 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Jul 2016 13:07:03 +0200 Subject: [PATCH 001/198] Extract jobs config to separate key in config hash --- lib/gitlab/ci/config/node/global.rb | 16 ++++++++++++++++ spec/lib/gitlab/ci/config/node/global_spec.rb | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index f92e1eccbcf..0a4315db047 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -36,6 +36,22 @@ module Gitlab helpers :before_script, :image, :services, :after_script, :variables, :stages, :types, :cache + def initialize(config) + return super(config) unless config.is_a?(Hash) + + jobs = config.except(*self.class.nodes.keys) + global = config.slice(*self.class.nodes.keys) + + super(global.merge(jobs: jobs)) + end + + ## + # Temporary refactoring stub + # + def jobs + @config[:jobs] + end + def stages stages_defined? ? stages_value : types_value end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index c87c9e97bc8..88194bf9453 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -22,7 +22,9 @@ describe Gitlab::Ci::Config::Node::Global do variables: { VAR: 'value' }, after_script: ['make clean'], stages: ['build', 'pages'], - cache: { key: 'k', untracked: true, paths: ['public/'] } } + cache: { key: 'k', untracked: true, paths: ['public/'] }, + rspec: { script: 'rspec' }, + spinach: { script: 'spinach' } } end describe '#process!' do @@ -120,6 +122,14 @@ describe Gitlab::Ci::Config::Node::Global do .to eq(key: 'k', untracked: true, paths: ['public/']) end end + + describe '#jobs' do + it 'returns jobs configuration' do + expect(global.jobs) + .to eq(rspec: { script: 'rspec' }, + spinach: { script: 'spinach' }) + end + end end end From 9686a4f2b2fd010b5a15409ba65ac4eed8e11c7b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Jul 2016 13:19:22 +0200 Subject: [PATCH 002/198] Remove code creating job hash from legacy CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 5 +++-- lib/gitlab/ci/config.rb | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 01ef13df57a..8d0bbe1ae56 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -68,8 +68,9 @@ module Ci @jobs = {} - @config.except!(*ALLOWED_YAML_KEYS) - @config.each { |name, param| add_job(name, param) } + @ci_config.jobs.each do |name, param| + add_job(name, param) + end raise ValidationError, "Please define at least one job" if @jobs.none? end diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index e6cc1529760..ae82c0db3f1 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -8,7 +8,7 @@ module Gitlab # Temporary delegations that should be removed after refactoring # delegate :before_script, :image, :services, :after_script, :variables, - :stages, :cache, to: :@global + :stages, :cache, :jobs, to: :@global def initialize(config) @config = Loader.new(config).load! From 5b7f211cbba06f7c43b55b4a38704073564a099f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Jul 2016 13:35:50 +0200 Subject: [PATCH 003/198] Add new CI config entry that holds jobs definition --- lib/gitlab/ci/config/node/global.rb | 14 +++----- lib/gitlab/ci/config/node/jobs.rb | 18 ++++++++++ spec/lib/gitlab/ci/config/node/global_spec.rb | 4 +-- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 36 +++++++++++++++++++ 4 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 lib/gitlab/ci/config/node/jobs.rb create mode 100644 spec/lib/gitlab/ci/config/node/jobs_spec.rb diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index 0a4315db047..cb2db4e9757 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -33,11 +33,14 @@ module Gitlab node :cache, Node::Cache, description: 'Configure caching between build jobs.' + node :jobs, Node::Jobs, + description: 'Definition of jobs for this pipeline.' + helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache + :variables, :stages, :types, :cache, :jobs def initialize(config) - return super(config) unless config.is_a?(Hash) + return super unless config.is_a?(Hash) jobs = config.except(*self.class.nodes.keys) global = config.slice(*self.class.nodes.keys) @@ -45,13 +48,6 @@ module Gitlab super(global.merge(jobs: jobs)) end - ## - # Temporary refactoring stub - # - def jobs - @config[:jobs] - end - def stages stages_defined? ? stages_value : types_value end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb new file mode 100644 index 00000000000..2df2d55aca2 --- /dev/null +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -0,0 +1,18 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a set of jobs. + # + class Jobs < Entry + include Validatable + + validations do + validates :config, type: Hash + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 88194bf9453..8f2f9e171d1 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -35,7 +35,7 @@ describe Gitlab::Ci::Config::Node::Global do end it 'creates node object for each entry' do - expect(global.nodes.count).to eq 8 + expect(global.nodes.count).to eq 9 end it 'creates node object using valid class' do @@ -139,7 +139,7 @@ describe Gitlab::Ci::Config::Node::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.nodes.count).to eq 8 + expect(global.nodes.count).to eq 9 end it 'contains undefined nodes' do diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb new file mode 100644 index 00000000000..3afa3de06c6 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Jobs do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { rspec: { script: 'rspec' } } } + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(rspec: { script: 'rspec' }) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + 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 'jobs config should be a hash' + end + end + end + end + end +end From e00ae9a87720bccda634dc85c018f5ec1d03a883 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Jul 2016 14:04:22 +0200 Subject: [PATCH 004/198] Add new CI config entry for single job in pipeline --- lib/gitlab/ci/config/node/job.rb | 14 +++++++++ spec/lib/gitlab/ci/config/node/job_spec.rb | 36 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 lib/gitlab/ci/config/node/job.rb create mode 100644 spec/lib/gitlab/ci/config/node/job_spec.rb diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb new file mode 100644 index 00000000000..5be8cb39a87 --- /dev/null +++ b/lib/gitlab/ci/config/node/job.rb @@ -0,0 +1,14 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a concrete CI/CD job. + # + class Job < Entry + include Configurable + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb new file mode 100644 index 00000000000..7dd25a23c83 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Job do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { script: 'rspec' } } + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(script: 'rspec') + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + 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 'job config should be a hash' + end + end + end + end + end +end From 6ae80732bb3b503e2d15acb2cab527c17e22e34b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Jul 2016 14:17:09 +0200 Subject: [PATCH 005/198] Add ability to define nodes in new CI config entry --- lib/gitlab/ci/config/node/entry.rb | 18 +++++++++++------- spec/lib/gitlab/ci/config/node/global_spec.rb | 16 ++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 9e79e170a4f..7148f4c2a79 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -26,12 +26,16 @@ module Gitlab process_nodes! end - def nodes - @nodes.values + def leaf? + nodes.none? end - def leaf? - self.class.nodes.none? + def nodes + self.class.nodes + end + + def descendants + @nodes.values end def ancestors @@ -43,7 +47,7 @@ module Gitlab end def errors - @validator.messages + nodes.flat_map(&:errors) + @validator.messages + @nodes.values.flat_map(&:errors) end def value @@ -73,13 +77,13 @@ module Gitlab private def compose! - self.class.nodes.each do |key, essence| + nodes.each do |key, essence| @nodes[key] = create_node(key, essence) end end def process_nodes! - nodes.each(&:process!) + @nodes.each_value(&:process!) end def create_node(key, essence) diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 8f2f9e171d1..254cb28190c 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -31,24 +31,24 @@ describe Gitlab::Ci::Config::Node::Global do before { global.process! } it 'creates nodes hash' do - expect(global.nodes).to be_an Array + expect(global.descendants).to be_an Array end it 'creates node object for each entry' do - expect(global.nodes.count).to eq 9 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do - expect(global.nodes.first) + expect(global.descendants.first) .to be_an_instance_of Gitlab::Ci::Config::Node::Script - expect(global.nodes.second) + expect(global.descendants.second) .to be_an_instance_of Gitlab::Ci::Config::Node::Image end it 'sets correct description for nodes' do - expect(global.nodes.first.description) + expect(global.descendants.first.description) .to eq 'Script that will be executed before each job.' - expect(global.nodes.second.description) + expect(global.descendants.second.description) .to eq 'Docker image that will be used to execute jobs.' end end @@ -139,11 +139,11 @@ describe Gitlab::Ci::Config::Node::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.nodes.count).to eq 9 + expect(global.descendants.count).to eq 9 end it 'contains undefined nodes' do - expect(global.nodes.first) + expect(global.descendants.first) .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined end end From dbab56a9519039bb6a83974c31b90b1283b8479c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Jul 2016 14:48:17 +0200 Subject: [PATCH 006/198] Create composite job entries in new CI config --- lib/gitlab/ci/config/node/jobs.rb | 14 ++++++++++++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 ++-- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 2df2d55aca2..915b46652f2 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -11,6 +11,20 @@ module Gitlab validations do validates :config, type: Hash end + + def nodes + @config + end + + private + + def create_node(key, essence) + Node::Job.new(essence).tap do |job| + job.key = key + job.parent = self + job.description = "#{key} job definition." + end + end end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index bad439bc489..49a786191b8 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1043,11 +1043,11 @@ EOT end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") end - it "returns errors if there are unknown parameters" do + it "returns error if job configuration is invalid" do config = YAML.dump({ extra: "bundle update" }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") end it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 3afa3de06c6..7f80e11cea3 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -4,6 +4,8 @@ describe Gitlab::Ci::Config::Node::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do + before { entry.process! } + context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } @@ -33,4 +35,19 @@ describe Gitlab::Ci::Config::Node::Jobs do end end end + + describe '#descendants' do + before { entry.process! } + + let(:config) do + { rspec: { script: 'rspec' }, + spinach: { script: 'spinach' } } + end + + it 'creates two descendant nodes' do + expect(entry.descendants.count).to eq 2 + expect(entry.descendants) + .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) + end + end end From b1b0c18b8c66857964eaaa5704a0744aacb707dd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 6 Jul 2016 12:58:43 +0200 Subject: [PATCH 007/198] Add hidden job in new CI config that is irrelevant --- lib/gitlab/ci/config/node/entry.rb | 11 ++++- lib/gitlab/ci/config/node/hidden_job.rb | 22 +++++++++ lib/gitlab/ci/config/node/jobs.rb | 10 +++- lib/gitlab/ci/config/node/validator.rb | 2 +- .../gitlab/ci/config/node/hidden_job_spec.rb | 48 +++++++++++++++++++ spec/lib/gitlab/ci/config/node/job_spec.rb | 6 +++ spec/lib/gitlab/ci/config/node/jobs_spec.rb | 23 ++++++--- 7 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 lib/gitlab/ci/config/node/hidden_job.rb create mode 100644 spec/lib/gitlab/ci/config/node/hidden_job_spec.rb diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 7148f4c2a79..fa6569e8bb2 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -54,8 +54,11 @@ module Gitlab if leaf? @config else - defined = @nodes.select { |_key, value| value.defined? } - Hash[defined.map { |key, node| [key, node.value] }] + meaningful = @nodes.select do |_key, value| + value.defined? && value.relevant? + end + + Hash[meaningful.map { |key, node| [key, node.value] }] end end @@ -63,6 +66,10 @@ module Gitlab true end + def relevant? + true + end + def self.default end diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden_job.rb new file mode 100644 index 00000000000..6a559ee8c04 --- /dev/null +++ b/lib/gitlab/ci/config/node/hidden_job.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a hidden CI/CD job. + # + class HiddenJob < Entry + include Validatable + + validations do + validates :config, type: Hash + end + + def relevant? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 915b46652f2..a76b7a260c4 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -19,12 +19,20 @@ module Gitlab private def create_node(key, essence) - Node::Job.new(essence).tap do |job| + fabricate_job(key, essence).tap do |job| job.key = key job.parent = self job.description = "#{key} job definition." end end + + def fabricate_job(key, essence) + if key.to_s.start_with?('.') + Node::HiddenJob.new(essence) + else + Node::Job.new(essence) + end + end end end end diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb index 758a6cf4356..dcfeb194374 100644 --- a/lib/gitlab/ci/config/node/validator.rb +++ b/lib/gitlab/ci/config/node/validator.rb @@ -31,7 +31,7 @@ module Gitlab def location predecessors = ancestors.map(&:key).compact - current = key || @node.class.name.demodulize.underscore + current = key || @node.class.name.demodulize.underscore.humanize predecessors.append(current).join(':') end 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 new file mode 100644 index 00000000000..ab865c3522e --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::HiddenJob do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { image: 'ruby:2.2' } } + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(image: 'ruby:2.2') + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + 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 + end + end + + describe '#leaf?' do + it 'is a leaf' do + expect(entry).to be_leaf + end + end + + describe '#relevant?' do + it 'is not a relevant entry' do + expect(entry).not_to be_relevant + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 7dd25a23c83..15c7f9bc394 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -33,4 +33,10 @@ describe Gitlab::Ci::Config::Node::Job do end end end + + describe '#relevant?' do + it 'is a relevant entry' do + expect(entry).to be_relevant + end + end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 7f80e11cea3..b2d2a92d9e8 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -36,18 +36,29 @@ describe Gitlab::Ci::Config::Node::Jobs do end end - describe '#descendants' do + context 'when valid job entries processed' do before { entry.process! } let(:config) do { rspec: { script: 'rspec' }, - spinach: { script: 'spinach' } } + spinach: { script: 'spinach' }, + '.hidden'.to_sym => {} } end - it 'creates two descendant nodes' do - expect(entry.descendants.count).to eq 2 - expect(entry.descendants) - .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) + describe '#descendants' do + it 'creates valid descendant nodes' do + expect(entry.descendants.count).to eq 3 + 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) + end + end + + describe '#value' do + it 'returns value of visible jobs only' do + expect(entry.value.keys).to eq [:rspec, :spinach] + end end end end From 580c4e1841cf4756e86c1ec9eddef56e2bfc9c97 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 6 Jul 2016 13:08:40 +0200 Subject: [PATCH 008/198] Move decision about relevant jobs to new CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 8d0bbe1ae56..d226ebc3229 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -76,8 +76,6 @@ module Ci end def add_job(name, job) - return if name.to_s.start_with?('.') - raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) stage = job[:stage] || job[:type] || DEFAULT_STAGE From 4491bf28e10da258701b316f397c5802f5f9974e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 6 Jul 2016 14:08:19 +0200 Subject: [PATCH 009/198] Move CI job config validations to new classes --- lib/ci/gitlab_ci_yaml_processor.rb | 2 - lib/gitlab/ci/config/node/entry.rb | 1 + lib/gitlab/ci/config/node/jobs.rb | 15 +++++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 9 +++- spec/lib/gitlab/ci/config/node/global_spec.rb | 7 +++- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 42 +++++++++++++------ 6 files changed, 59 insertions(+), 17 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index d226ebc3229..ab77d4df841 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -71,8 +71,6 @@ module Ci @ci_config.jobs.each do |name, param| add_job(name, param) end - - raise ValidationError, "Please define at least one job" if @jobs.none? end def add_job(name, job) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index fa6569e8bb2..033f9f0e3d1 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -24,6 +24,7 @@ module Gitlab compose! process_nodes! + @validator.validate(:processed) end def leaf? diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index a76b7a260c4..e8e78e4088d 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -10,12 +10,27 @@ module Gitlab validations do validates :config, type: Hash + validate :jobs_presence, on: :processed + + def jobs_presence + unless relevant? + errors.add(:config, 'should contain at least one visible job') + end + end end def nodes @config end + def relevant? + @nodes.values.any?(&:relevant?) + end + + def leaf? + false + end + private def create_node(key, essence) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 49a786191b8..ac058ba1595 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1061,7 +1061,14 @@ EOT config = YAML.dump({ before_script: ["bundle update"] }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if there are no visible jobs defined" do + config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => {} }) + expect do + GitlabCiYamlProcessor.new(config, path) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") end it "returns errors if job allow_failure parameter is not an boolean" do diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 254cb28190c..a98de73c06c 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -108,7 +108,10 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when deprecated types key defined' do - let(:hash) { { types: ['test', 'deploy'] } } + let(:hash) do + { types: ['test', 'deploy'], + rspec: { script: 'rspec' } } + end it 'returns array of types as stages' do expect(global.stages).to eq %w[test deploy] @@ -174,7 +177,7 @@ describe Gitlab::Ci::Config::Node::Global do # details. # context 'when entires specified but not defined' do - let(:hash) { { variables: nil } } + let(:hash) { { variables: nil, rspec: { script: 'rspec' } } } before { global.process! } describe '#variables' do diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index b2d2a92d9e8..7ec28b642b4 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -4,17 +4,9 @@ describe Gitlab::Ci::Config::Node::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.process! } - context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } - describe '#value' do - it 'returns key value' do - expect(entry.value).to eq(rspec: { script: 'rspec' }) - end - end - describe '#valid?' do it 'is valid' do expect(entry).to be_valid @@ -23,15 +15,34 @@ describe Gitlab::Ci::Config::Node::Jobs do end context 'when entry value is not correct' do - context 'incorrect config value type' do - let(:config) { ['incorrect'] } + describe '#errors' do + context 'incorrect config value type' do + let(:config) { ['incorrect'] } - describe '#errors' do - it 'saves errors' do + it 'returns error about incorrect type' do expect(entry.errors) .to include 'jobs config should be a hash' end end + + context 'when no visible jobs present' do + let(:config) { { '.hidden'.to_sym => {} } } + + context 'when not processed' do + it 'is valid' do + expect(entry.errors).to be_empty + end + end + + context 'when processed' do + before { entry.process! } + + it 'returns error about no visible jobs defined' do + expect(entry.errors) + .to include 'jobs config should contain at least one visible job' + end + end + end end end end @@ -45,6 +56,13 @@ describe Gitlab::Ci::Config::Node::Jobs do '.hidden'.to_sym => {} } end + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(rspec: { script: 'rspec' }, + spinach: { script: 'spinach' }) + end + end + describe '#descendants' do it 'creates valid descendant nodes' do expect(entry.descendants.count).to eq 3 From 6f02da2c4e069ef4ab550dc43176dc0563c95017 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 6 Jul 2016 14:24:31 +0200 Subject: [PATCH 010/198] Simplify legacy CI config processor a little --- lib/ci/gitlab_ci_yaml_processor.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index ab77d4df841..f0710690985 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -26,7 +26,6 @@ module Ci end initial_parsing - validate! rescue Gitlab::Ci::Config::Loader::FormatError => e raise ValidationError, e.message end @@ -71,6 +70,10 @@ module Ci @ci_config.jobs.each do |name, param| add_job(name, param) end + + @jobs.each do |name, job| + validate_job!(name, job) + end end def add_job(name, job) @@ -108,14 +111,6 @@ module Ci } end - def validate! - @jobs.each do |name, job| - validate_job!(name, job) - end - - true - end - def validate_job!(name, job) validate_job_name!(name) validate_job_keys!(name, job) From b0ae0d730f4eda34cd712477a828dddd888ba474 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 10:23:47 +0200 Subject: [PATCH 011/198] Use only node factory to create CI config entries --- lib/gitlab/ci/config/node/factory.rb | 28 +++++++++++-------- lib/gitlab/ci/config/node/global.rb | 4 +-- lib/gitlab/ci/config/node/jobs.rb | 20 +++++-------- .../lib/gitlab/ci/config/node/factory_spec.rb | 12 ++++++++ 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 5919a283283..b1457b81a45 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -21,26 +21,30 @@ module Gitlab def create! raise InvalidFactory unless @attributes.has_key?(:value) - fabricate.tap do |entry| - entry.key = @attributes[:key] - entry.parent = @attributes[:parent] - entry.description = @attributes[:description] - end - end - - private - - def fabricate ## # We assume that unspecified entry is undefined. # See issue #18775. # if @attributes[:value].nil? - Node::Undefined.new(@node) + fabricate(Node::Undefined, @node) else - @node.new(@attributes[:value]) + fabricate(@node, @attributes[:value]) end end + + def self.fabricate(node, value, **attributes) + node.new(value).tap do |entry| + entry.key = attributes[:key] + entry.parent = attributes[:parent] + entry.description = attributes[:description] + end + end + + private + + def fabricate(node, value) + self.class.fabricate(node, value, @attributes) + end end end end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index cb2db4e9757..64d8e39093d 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -42,8 +42,8 @@ module Gitlab def initialize(config) return super unless config.is_a?(Hash) - jobs = config.except(*self.class.nodes.keys) - global = config.slice(*self.class.nodes.keys) + jobs = config.except(*nodes.keys) + global = config.slice(*nodes.keys) super(global.merge(jobs: jobs)) end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index e8e78e4088d..1cd2dc8f5b3 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -33,20 +33,14 @@ module Gitlab private - def create_node(key, essence) - fabricate_job(key, essence).tap do |job| - job.key = key - job.parent = self - job.description = "#{key} job definition." - end - end + def create_node(key, value) + node = key.to_s.start_with?('.') ? Node::HiddenJob : Node::Job - def fabricate_job(key, essence) - if key.to_s.start_with?('.') - Node::HiddenJob.new(essence) - else - Node::Job.new(essence) - end + attributes = { key: key, + parent: self, + description: "#{key} job definition." } + + Node::Factory.fabricate(node, value, attributes) end end end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index 91ddef7bfbf..5a26bb6df02 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -5,6 +5,18 @@ describe Gitlab::Ci::Config::Node::Factory do let(:factory) { described_class.new(entry_class) } let(:entry_class) { Gitlab::Ci::Config::Node::Script } + describe '.fabricate' do + it 'fabricates entry with attributes set' do + fabricated = described_class + .fabricate(entry_class, ['ls'], + parent: factory, key: :test) + + expect(fabricated.parent).to be factory + expect(fabricated.key).to eq :test + expect(fabricated.value).to eq ['ls'] + end + end + context 'when setting up a value' do it 'creates entry with valid value' do entry = factory From fea7762485c75003381891bc892bc6049f8a2105 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 12:41:31 +0200 Subject: [PATCH 012/198] Delegate methods to default CI entry if undefined --- lib/gitlab/ci/config/node/entry.rb | 5 ++ lib/gitlab/ci/config/node/undefined.rb | 29 +++++++- .../lib/gitlab/ci/config/node/factory_spec.rb | 6 +- .../gitlab/ci/config/node/undefined_spec.rb | 67 +++++++++++++------ 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 033f9f0e3d1..8fda37a8922 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -14,6 +14,7 @@ module Gitlab def initialize(config) @config = config @nodes = {} + @validator = self.class.validator.new(self) @validator.validate end @@ -71,6 +72,10 @@ module Gitlab true end + def attributes + { key: @key, parent: @parent, description: @description } + end + def self.default end diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index 699605e1e3a..f152c433c42 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -5,8 +5,9 @@ module Gitlab ## # This class represents an undefined entry node. # - # It takes original entry class as configuration and returns default - # value of original entry as self value. + # It takes original entry class as configuration and creates an object + # if original entry has a default value. If there is default value + # some methods are delegated to it. # # class Undefined < Entry @@ -16,13 +17,35 @@ module Gitlab validates :config, type: Class end + def initialize(node) + super + + unless node.default.nil? + @default = fabricate_default(node) + end + end + def value - @config.default + @default.value if @default + end + + def valid? + @default ? @default.valid? : true + end + + def errors + @default ? @default.errors : [] end def defined? false end + + private + + def fabricate_default(node) + Node::Factory.fabricate(node, node.default, attributes) + end end end end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index 5a26bb6df02..5b856d44989 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -9,11 +9,13 @@ describe Gitlab::Ci::Config::Node::Factory do it 'fabricates entry with attributes set' do fabricated = described_class .fabricate(entry_class, ['ls'], - parent: factory, key: :test) + parent: true, key: :test) - expect(fabricated.parent).to be factory + expect(fabricated.parent).to be true expect(fabricated.key).to eq :test expect(fabricated.value).to eq ['ls'] + expect(fabricated.attributes) + .to eq(parent: true, key: :test, description: nil) end end diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb index 0c6608d906d..417b4a0ad6f 100644 --- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb +++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb @@ -2,33 +2,62 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Undefined do let(:undefined) { described_class.new(entry) } - let(:entry) { Class.new } + let(:entry) { spy('Entry') } - describe '#leaf?' do - it 'is leaf node' do - expect(undefined).to be_leaf + context 'when entry does not have a default value' do + before { allow(entry).to receive(:default).and_return(nil) } + + describe '#leaf?' do + it 'is leaf node' do + expect(undefined).to be_leaf + end + end + + describe '#valid?' do + it 'is always valid' do + expect(undefined).to be_valid + end + end + + describe '#errors' do + it 'is does not contain errors' do + expect(undefined.errors).to be_empty + end + end + + describe '#value' do + it 'returns nil' do + expect(undefined.value).to eq nil + end end end - describe '#valid?' do - it 'is always valid' do - expect(undefined).to be_valid - end - end - - describe '#errors' do - it 'is does not contain errors' do - expect(undefined.errors).to be_empty - end - end - - describe '#value' do + context 'when entry has a default value' do before do allow(entry).to receive(:default).and_return('some value') + allow(entry).to receive(:value).and_return('some value') end - it 'returns default value for entry' do - expect(undefined.value).to eq 'some value' + describe '#value' do + it 'returns default value for entry' do + expect(undefined.value).to eq 'some value' + end + end + + describe '#errors' do + it 'delegates errors to default entry' do + expect(entry).to receive(:errors) + + undefined.errors + end + end + + describe '#valid?' do + it 'delegates valid? to default entry' do + expect(entry).to receive(:valid?) + + undefined.valid? + end end end From de8b93bbff65844438d7dfbde178746c3585bd92 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 12:56:00 +0200 Subject: [PATCH 013/198] Improve validation of CI config entry if composite --- lib/gitlab/ci/config/node/entry.rb | 12 ++++-------- lib/gitlab/ci/config/node/jobs.rb | 4 ---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 8fda37a8922..97e17b89c40 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -20,12 +20,8 @@ module Gitlab end def process! - return if leaf? - return unless valid? - - compose! - process_nodes! - @validator.validate(:processed) + compose! unless leaf? + @validator.validate(:processed) if valid? end def leaf? @@ -90,12 +86,12 @@ module Gitlab private def compose! + return unless valid? + nodes.each do |key, essence| @nodes[key] = create_node(key, essence) end - end - def process_nodes! @nodes.each_value(&:process!) end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 1cd2dc8f5b3..71893ba1d88 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -27,10 +27,6 @@ module Gitlab @nodes.values.any?(&:relevant?) end - def leaf? - false - end - private def create_node(key, value) From ecdcf04e88f6313ae8773e7b9886bc983adab83d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 13:09:36 +0200 Subject: [PATCH 014/198] Add undefined CI node strategies to handle defaults --- lib/gitlab/ci/config/node/undefined.rb | 55 +++++++++++++++++--------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index f152c433c42..7b18e364675 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -13,28 +13,15 @@ module Gitlab class Undefined < Entry include Validatable + delegate :valid?, :errors, :value, to: :@strategy + validations do validates :config, type: Class end def initialize(node) super - - unless node.default.nil? - @default = fabricate_default(node) - end - end - - def value - @default.value if @default - end - - def valid? - @default ? @default.valid? : true - end - - def errors - @default ? @default.errors : [] + @strategy = create_strategy(node, node.default) end def defined? @@ -43,8 +30,40 @@ module Gitlab private - def fabricate_default(node) - Node::Factory.fabricate(node, node.default, attributes) + def create_strategy(node, default) + if default.nil? + Undefined::NullStrategy.new + else + entry = Node::Factory + .fabricate(node, default, attributes) + + Undefined::DefaultStrategy.new(entry) + end + end + + class DefaultStrategy + delegate :valid?, :errors, :value, to: :@default + + def initialize(entry) + @default = entry + end + end + + class NullStrategy + def initialize(*) + end + + def value + nil + end + + def valid? + true + end + + def errors + [] + end end end end From 9410aecca87a1c03f7e7fb1e5e1073460c71b6e5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 13:39:13 +0200 Subject: [PATCH 015/198] Add scaffold of CI config for the job stage entry --- lib/gitlab/ci/config/node/stage.rb | 22 ++++++++++ spec/lib/gitlab/ci/config/node/stage_spec.rb | 46 ++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 lib/gitlab/ci/config/node/stage.rb create mode 100644 spec/lib/gitlab/ci/config/node/stage_spec.rb diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb new file mode 100644 index 00000000000..53ceafaa3f4 --- /dev/null +++ b/lib/gitlab/ci/config/node/stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a stage for a job. + # + class Stage < Entry + include Validatable + + validations do + validates :config, key: true + end + + def self.default + :test + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb new file mode 100644 index 00000000000..653d613ba6e --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Stage do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { :stage1 } + + describe '#value' do + it 'returns a stage key' do + expect(entry.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when entry config is incorrect' do + let(:config) { { test: true } } + + describe '#errors' do + it 'reports errors' do + expect(entry.errors) + .to include 'stage config should be a string or symbol' + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + end + end + + describe '.default' do + it 'returns default stage' do + expect(described_class.default).to eq :test + end + end +end From a7ac2f74944430d75b090f78cd9c1cf1d24379f6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 15:00:35 +0200 Subject: [PATCH 016/198] Simplify CI config entry node factory, use attribs --- lib/gitlab/ci/config/node/configurable.rb | 4 ++- lib/gitlab/ci/config/node/entry.rb | 14 +++++----- lib/gitlab/ci/config/node/factory.rb | 27 +++++++------------ lib/gitlab/ci/config/node/global.rb | 4 +++ lib/gitlab/ci/config/node/jobs.rb | 10 +++---- lib/gitlab/ci/config/node/undefined.rb | 6 ++--- .../lib/gitlab/ci/config/node/factory_spec.rb | 26 +++++------------- 7 files changed, 37 insertions(+), 54 deletions(-) diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 37936fc8242..88403a9de1e 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -26,7 +26,9 @@ module Gitlab private def create_node(key, factory) - factory.with(value: @config[key], key: key, parent: self) + factory + .value(config[key]) + .with(key: key, parent: self, global: global) factory.create! end diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 97e17b89c40..e8b0160edc1 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -8,13 +8,17 @@ module Gitlab class Entry class InvalidError < StandardError; end - attr_reader :config - attr_accessor :key, :parent, :description + attr_reader :config, :attributes + attr_accessor :key, :parent, :global, :description - def initialize(config) + def initialize(config, **attributes) @config = config @nodes = {} + (@attributes = attributes).each do |attribute, value| + public_send("#{attribute}=", value) + end + @validator = self.class.validator.new(self) @validator.validate end @@ -68,10 +72,6 @@ module Gitlab true end - def attributes - { key: @key, parent: @parent, description: @description } - end - def self.default end diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index b1457b81a45..3f2cdf436e3 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -13,38 +13,29 @@ module Gitlab @attributes = {} end + def value(value) + @value = value + self + end + def with(attributes) @attributes.merge!(attributes) self end def create! - raise InvalidFactory unless @attributes.has_key?(:value) + raise InvalidFactory unless defined?(@value) ## # We assume that unspecified entry is undefined. # See issue #18775. # - if @attributes[:value].nil? - fabricate(Node::Undefined, @node) + if @value.nil? + Node::Undefined.new(@node, @attributes) else - fabricate(@node, @attributes[:value]) + @node.new(@value, @attributes) end end - - def self.fabricate(node, value, **attributes) - node.new(value).tap do |entry| - entry.key = attributes[:key] - entry.parent = attributes[:parent] - entry.description = attributes[:description] - end - end - - private - - def fabricate(node, value) - self.class.fabricate(node, value, @attributes) - end end end end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index 64d8e39093d..dffa3326630 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -51,6 +51,10 @@ module Gitlab def stages stages_defined? ? stages_value : types_value end + + def global + self + end end end end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 71893ba1d88..d7d61ade36d 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -30,13 +30,13 @@ module Gitlab private def create_node(key, value) - node = key.to_s.start_with?('.') ? Node::HiddenJob : Node::Job + job_node = key.to_s.start_with?('.') ? Node::HiddenJob : Node::Job - attributes = { key: key, - parent: self, - description: "#{key} job definition." } + job_attributes = { key: key, + parent: self, + description: "#{key} job definition." } - Node::Factory.fabricate(node, value, attributes) + job_node.new(value, attributes.merge(job_attributes)) end end end diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index 7b18e364675..fedb9d020be 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -19,7 +19,7 @@ module Gitlab validates :config, type: Class end - def initialize(node) + def initialize(node, **attributes) super @strategy = create_strategy(node, node.default) end @@ -34,9 +34,7 @@ module Gitlab if default.nil? Undefined::NullStrategy.new else - entry = Node::Factory - .fabricate(node, default, attributes) - + entry = node.new(default, attributes) Undefined::DefaultStrategy.new(entry) end end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index 5b856d44989..c912b1b2044 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -5,24 +5,10 @@ describe Gitlab::Ci::Config::Node::Factory do let(:factory) { described_class.new(entry_class) } let(:entry_class) { Gitlab::Ci::Config::Node::Script } - describe '.fabricate' do - it 'fabricates entry with attributes set' do - fabricated = described_class - .fabricate(entry_class, ['ls'], - parent: true, key: :test) - - expect(fabricated.parent).to be true - expect(fabricated.key).to eq :test - expect(fabricated.value).to eq ['ls'] - expect(fabricated.attributes) - .to eq(parent: true, key: :test, description: nil) - end - end - context 'when setting up a value' do it 'creates entry with valid value' do entry = factory - .with(value: ['ls', 'pwd']) + .value(['ls', 'pwd']) .create! expect(entry.value).to eq ['ls', 'pwd'] @@ -31,7 +17,7 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when setting description' do it 'creates entry with description' do entry = factory - .with(value: ['ls', 'pwd']) + .value(['ls', 'pwd']) .with(description: 'test description') .create! @@ -43,7 +29,8 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when setting key' do it 'creates entry with custom key' do entry = factory - .with(value: ['ls', 'pwd'], key: 'test key') + .value(['ls', 'pwd']) + .with(key: 'test key') .create! expect(entry.key).to eq 'test key' @@ -55,7 +42,8 @@ describe Gitlab::Ci::Config::Node::Factory do it 'creates entry with valid parent' do entry = factory - .with(value: 'ls', parent: parent) + .value('ls') + .with(parent: parent) .create! expect(entry.parent).to eq parent @@ -74,7 +62,7 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when creating entry with nil value' do it 'creates an undefined entry' do entry = factory - .with(value: nil) + .value(nil) .create! expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined From f067202e9b7a4304ffb8d68408880f7eb7fc8b34 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 15:06:05 +0200 Subject: [PATCH 017/198] Improve creating CI config entries for jobs config --- lib/gitlab/ci/config/node/jobs.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index d7d61ade36d..cba1fce4a4c 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -29,14 +29,18 @@ module Gitlab private - def create_node(key, value) - job_node = key.to_s.start_with?('.') ? Node::HiddenJob : Node::Job + def create_node(name, config) + job_node(name).new(config, job_attributes(name)) + end - job_attributes = { key: key, - parent: self, - description: "#{key} job definition." } + def job_node(name) + name.to_s.start_with?('.') ? Node::HiddenJob : Node::Job + end - job_node.new(value, attributes.merge(job_attributes)) + def job_attributes(name) + @attributes.merge(key: name, + parent: self, + description: "#{name} job definition.") end end end From 3da57c800bf0c4fe3c45dbea3cff4179f6aa124f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 7 Jul 2016 15:15:44 +0200 Subject: [PATCH 018/198] Require reference to CI config for some entries --- lib/gitlab/ci/config/node/stage.rb | 7 +++++ spec/lib/gitlab/ci/config/node/stage_spec.rb | 31 ++++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb index 53ceafaa3f4..c7e12f291d7 100644 --- a/lib/gitlab/ci/config/node/stage.rb +++ b/lib/gitlab/ci/config/node/stage.rb @@ -10,6 +10,13 @@ module Gitlab validations do validates :config, key: true + + validate do |entry| + unless entry.global + raise Entry::InvalidError, + 'This entry needs reference to global configuration' + end + end end def self.default diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb index 653d613ba6e..92150ea5337 100644 --- a/spec/lib/gitlab/ci/config/node/stage_spec.rb +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -1,11 +1,12 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Stage do - let(:entry) { described_class.new(config) } + let(:entry) { described_class.new(config, global: global) } + let(:global) { spy('Global') } describe 'validations' do context 'when entry config value is correct' do - let(:config) { :stage1 } + let(:config) { :build } describe '#value' do it 'returns a stage key' do @@ -18,20 +19,32 @@ describe Gitlab::Ci::Config::Node::Stage do expect(entry).to be_valid end end + end - context 'when entry config is incorrect' do - let(:config) { { test: true } } + context 'when entry config is incorrect' do + describe '#errors' do + context 'when reference to global node is not set' do + let(:entry) { described_class.new(config) } - describe '#errors' do - it 'reports errors' do + it 'raises error' do + expect { entry } + .to raise_error Gitlab::Ci::Config::Node::Entry::InvalidError + end + end + + context 'when value has a wrong type' do + let(:config) { { test: true } } + + it 'reports errors about wrong type' do expect(entry.errors) .to include 'stage config should be a string or symbol' end end - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid + context 'when stage is not present in global configuration' do + pending 'reports error about missing stage' do + expect(entry.errors) + .to include 'stage config should be one of test, stage' end end end From 8baee987beaea8197d28ee9715ef23f5813566e5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 8 Jul 2016 11:27:36 +0200 Subject: [PATCH 019/198] Extract internal attributes validator for CI entry --- lib/gitlab/ci/config/node/stage.rb | 8 +------- lib/gitlab/ci/config/node/validators.rb | 9 +++++++++ spec/lib/gitlab/ci/config/node/stage_spec.rb | 6 ++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb index c7e12f291d7..9c76cf7c0b7 100644 --- a/lib/gitlab/ci/config/node/stage.rb +++ b/lib/gitlab/ci/config/node/stage.rb @@ -10,13 +10,7 @@ module Gitlab validations do validates :config, key: true - - validate do |entry| - unless entry.global - raise Entry::InvalidError, - 'This entry needs reference to global configuration' - end - end + validates :global, required_attribute: true end def self.default diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index 7b2f57990b5..6f0e14e2f0a 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -33,6 +33,15 @@ module Gitlab end end + class RequiredAttributeValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if value.nil? + raise Entry::InvalidError, + "Entry needs #{attribute} attribute set internally." + end + end + end + class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb index 92150ea5337..4047d46c80f 100644 --- a/spec/lib/gitlab/ci/config/node/stage_spec.rb +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -27,8 +27,10 @@ describe Gitlab::Ci::Config::Node::Stage do let(:entry) { described_class.new(config) } it 'raises error' do - expect { entry } - .to raise_error Gitlab::Ci::Config::Node::Entry::InvalidError + expect { entry }.to raise_error( + Gitlab::Ci::Config::Node::Entry::InvalidError, + /Entry needs global attribute set internally./ + ) end end From 159aed1c4220277ece9b4496d460f931cd65e228 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 8 Jul 2016 11:29:03 +0200 Subject: [PATCH 020/198] Extract global CI config entry configuration setup --- lib/gitlab/ci/config/node/global.rb | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index dffa3326630..110d982588b 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -39,21 +39,24 @@ module Gitlab helpers :before_script, :image, :services, :after_script, :variables, :stages, :types, :cache, :jobs - def initialize(config) - return super unless config.is_a?(Hash) - - jobs = config.except(*nodes.keys) - global = config.slice(*nodes.keys) - - super(global.merge(jobs: jobs)) + def initialize(config, **attributes) + super(setup(config), attributes) + @global = self end def stages stages_defined? ? stages_value : types_value end - def global - self + private + + def setup(config) + return config unless config.is_a?(Hash) + + jobs = config.except(*nodes.keys) + global = config.slice(*nodes.keys) + + global.merge(jobs: jobs) end end end From 1ac62de2c12a26e6f5158cdb4f008a71729b39fc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 8 Jul 2016 12:51:47 +0200 Subject: [PATCH 021/198] Extract CI entry node validator and improve naming --- lib/gitlab/ci/config.rb | 1 + lib/gitlab/ci/config/node/configurable.rb | 6 ++-- lib/gitlab/ci/config/node/entry.rb | 36 ++++++++++----------- lib/gitlab/ci/config/node/jobs.rb | 6 ++-- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 5 ++- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index ae82c0db3f1..20f5f8e2ff8 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -15,6 +15,7 @@ module Gitlab @global = Node::Global.new(@config) @global.process! + @global.validate! end def valid? diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 88403a9de1e..e5780c60e70 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -25,7 +25,7 @@ module Gitlab private - def create_node(key, factory) + def create(key, factory) factory .value(config[key]) .with(key: key, parent: self, global: global) @@ -50,12 +50,12 @@ module Gitlab def helpers(*nodes) nodes.each do |symbol| define_method("#{symbol}_defined?") do - @nodes[symbol].try(:defined?) + @entries[symbol].try(:defined?) end define_method("#{symbol}_value") do raise Entry::InvalidError unless valid? - @nodes[symbol].try(:value) + @entries[symbol].try(:value) end alias_method symbol.to_sym, "#{symbol}_value".to_sym diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index e8b0160edc1..67e59ffb86e 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -13,7 +13,7 @@ module Gitlab def initialize(config, **attributes) @config = config - @nodes = {} + @entries = {} (@attributes = attributes).each do |attribute, value| public_send("#{attribute}=", value) @@ -24,8 +24,18 @@ module Gitlab end def process! - compose! unless leaf? - @validator.validate(:processed) if valid? + return unless valid? + + nodes.each do |key, essence| + @entries[key] = create(key, essence) + end + + @entries.each_value(&:process!) + end + + def validate! + @validator.validate(:after) + @entries.each_value(&:validate!) end def leaf? @@ -37,7 +47,7 @@ module Gitlab end def descendants - @nodes.values + @entries.values end def ancestors @@ -49,18 +59,18 @@ module Gitlab end def errors - @validator.messages + @nodes.values.flat_map(&:errors) + @validator.messages + @entries.values.flat_map(&:errors) end def value if leaf? @config else - meaningful = @nodes.select do |_key, value| + meaningful = @entries.select do |_key, value| value.defined? && value.relevant? end - Hash[meaningful.map { |key, node| [key, node.value] }] + Hash[meaningful.map { |key, entry| [key, entry.value] }] end end @@ -85,17 +95,7 @@ module Gitlab private - def compose! - return unless valid? - - nodes.each do |key, essence| - @nodes[key] = create_node(key, essence) - end - - @nodes.each_value(&:process!) - end - - def create_node(key, essence) + def create(entry, essence) raise NotImplementedError end end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index cba1fce4a4c..6199749a508 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -10,7 +10,7 @@ module Gitlab validations do validates :config, type: Hash - validate :jobs_presence, on: :processed + validate :jobs_presence, on: :after def jobs_presence unless relevant? @@ -24,12 +24,12 @@ module Gitlab end def relevant? - @nodes.values.any?(&:relevant?) + @entries.values.any?(&:relevant?) end private - def create_node(name, config) + def create(name, config) job_node(name).new(config, job_attributes(name)) end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 7ec28b642b4..1ecc3e18d4e 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -35,7 +35,10 @@ describe Gitlab::Ci::Config::Node::Jobs do end context 'when processed' do - before { entry.process! } + before do + entry.process! + entry.validate! + end it 'returns error about no visible jobs defined' do expect(entry.errors) From d9142f2c97524fc2d5af7dda79b849d1e23f4910 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 8 Jul 2016 13:31:41 +0200 Subject: [PATCH 022/198] Add CI config known stage validation for job stage --- lib/gitlab/ci/config/node/stage.rb | 13 +++++ spec/lib/gitlab/ci/config/node/stage_spec.rb | 54 ++++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb index 9c76cf7c0b7..457f6dfa3ba 100644 --- a/lib/gitlab/ci/config/node/stage.rb +++ b/lib/gitlab/ci/config/node/stage.rb @@ -11,6 +11,19 @@ module Gitlab validations do validates :config, key: true validates :global, required_attribute: true + validate :known_stage, on: :after + + def known_stage + unless known? + stages_list = global.stages.join(', ') + errors.add(:config, + "should be one of defined stages (#{stages_list})") + end + end + end + + def known? + @global.stages.include?(@config) end def self.default diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb index 4047d46c80f..95b46d76adb 100644 --- a/spec/lib/gitlab/ci/config/node/stage_spec.rb +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -1,33 +1,33 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Stage do - let(:entry) { described_class.new(config, global: global) } + let(:stage) { described_class.new(config, global: global) } let(:global) { spy('Global') } describe 'validations' do - context 'when entry config value is correct' do + context 'when stage config value is correct' do let(:config) { :build } describe '#value' do it 'returns a stage key' do - expect(entry.value).to eq config + expect(stage.value).to eq config end end describe '#valid?' do it 'is valid' do - expect(entry).to be_valid + expect(stage).to be_valid end end end - context 'when entry config is incorrect' do + context 'when stage config is incorrect' do describe '#errors' do context 'when reference to global node is not set' do - let(:entry) { described_class.new(config) } + let(:stage) { described_class.new(config) } it 'raises error' do - expect { entry }.to raise_error( + expect { stage }.to raise_error( Gitlab::Ci::Config::Node::Entry::InvalidError, /Entry needs global attribute set internally./ ) @@ -38,21 +38,53 @@ describe Gitlab::Ci::Config::Node::Stage do let(:config) { { test: true } } it 'reports errors about wrong type' do - expect(entry.errors) + expect(stage.errors) .to include 'stage config should be a string or symbol' end end context 'when stage is not present in global configuration' do - pending 'reports error about missing stage' do - expect(entry.errors) - .to include 'stage config should be one of test, stage' + let(:config) { :unknown } + + before do + allow(global) + .to receive(:stages).and_return([:test, :deploy]) + end + + it 'reports error about missing stage' do + stage.validate! + + expect(stage.errors) + .to include 'stage config should be one of ' \ + 'defined stages (test, deploy)' end end end end end + describe '#known?' do + before do + allow(global).to receive(:stages).and_return([:test, :deploy]) + end + + context 'when stage is not known' do + let(:config) { :unknown } + + it 'returns false' do + expect(stage.known?).to be false + end + end + + context 'when stage is known' do + let(:config) { :test } + + it 'returns false' do + expect(stage.known?).to be true + end + end + end + describe '.default' do it 'returns default stage' do expect(described_class.default).to eq :test From 1b624ba4eb423c39671b9f0440018e9783aa844b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 8 Jul 2016 13:36:01 +0200 Subject: [PATCH 023/198] Improve name of CI entry validation context hook --- lib/gitlab/ci/config/node/entry.rb | 2 +- lib/gitlab/ci/config/node/jobs.rb | 2 +- lib/gitlab/ci/config/node/stage.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 67e59ffb86e..2b16a81f88d 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -34,7 +34,7 @@ module Gitlab end def validate! - @validator.validate(:after) + @validator.validate(:processed) @entries.each_value(&:validate!) end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 6199749a508..f6acc25e4fb 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -10,7 +10,7 @@ module Gitlab validations do validates :config, type: Hash - validate :jobs_presence, on: :after + validate :jobs_presence, on: :processed def jobs_presence unless relevant? diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb index 457f6dfa3ba..c15f46bc7a5 100644 --- a/lib/gitlab/ci/config/node/stage.rb +++ b/lib/gitlab/ci/config/node/stage.rb @@ -11,7 +11,7 @@ module Gitlab validations do validates :config, key: true validates :global, required_attribute: true - validate :known_stage, on: :after + validate :known_stage, on: :processed def known_stage unless known? From ccbdb4022ac87f7c30e970922be64bcea0b406e9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 9 Jul 2016 14:56:41 +0200 Subject: [PATCH 024/198] Integrate CI job stage entry into CI configuration --- lib/gitlab/ci/config/node/entry.rb | 18 +++++++++----- lib/gitlab/ci/config/node/job.rb | 24 +++++++++++++++++++ lib/gitlab/ci/config/node/stage.rb | 4 ++-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 6 ++--- spec/lib/gitlab/ci/config/node/global_spec.rb | 4 ++-- spec/lib/gitlab/ci/config/node/job_spec.rb | 12 ++++++++-- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 7 +++--- spec/lib/gitlab/ci/config/node/stage_spec.rb | 18 ++++++++------ 8 files changed, 68 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 2b16a81f88d..1940c39087b 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -20,21 +20,21 @@ module Gitlab end @validator = self.class.validator.new(self) - @validator.validate + @validator.validate(:new) end def process! return unless valid? - nodes.each do |key, essence| - @entries[key] = create(key, essence) - end - + compose! @entries.each_value(&:process!) end def validate! - @validator.validate(:processed) + if @validator.valid?(:new) + @validator.validate(:processed) + end + @entries.each_value(&:validate!) end @@ -95,6 +95,12 @@ module Gitlab private + def compose! + nodes.each do |key, essence| + @entries[key] = create(key, essence) + end + end + def create(entry, essence) raise NotImplementedError end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 5be8cb39a87..4a9cc28d763 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -7,6 +7,30 @@ module Gitlab # class Job < Entry include Configurable + + node :stage, Stage, + description: 'Pipeline stage this job will be executed into.' + + node :type, Stage, + description: 'Deprecated: stage this job will be executed into.' + + helpers :stage, :type + + def value + @config.merge(stage: stage_value) + end + + private + + def compose! + super + + if type_defined? && !stage_defined? + @entries[:stage] = @entries[:type] + end + + @entries.delete(:type) + end end end end diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb index c15f46bc7a5..e8fae65a2a9 100644 --- a/lib/gitlab/ci/config/node/stage.rb +++ b/lib/gitlab/ci/config/node/stage.rb @@ -9,7 +9,7 @@ module Gitlab include Validatable validations do - validates :config, key: true + validates :config, type: String validates :global, required_attribute: true validate :known_stage, on: :processed @@ -27,7 +27,7 @@ module Gitlab end def self.default - :test + 'test' end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ac058ba1595..03477e1ca13 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1082,21 +1082,21 @@ EOT config = YAML.dump({ rspec: { script: "test", type: 1 } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string") end it "returns errors if job stage is not a pre-defined stage" do config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be one of defined stages (build, test, deploy)") end it "returns errors if job stage is not a defined stage" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be one of defined stages (build, test)") end it "returns errors if stages is not an array" do diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index a98de73c06c..0c56b59db0c 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -129,8 +129,8 @@ describe Gitlab::Ci::Config::Node::Global do describe '#jobs' do it 'returns jobs configuration' do expect(global.jobs) - .to eq(rspec: { script: 'rspec' }, - spinach: { script: 'spinach' }) + .to eq(rspec: { script: 'rspec', stage: 'test' }, + spinach: { script: 'spinach', stage: 'test' }) end end end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 15c7f9bc394..2a4296448fb 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -1,15 +1,23 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Job do - let(:entry) { described_class.new(config) } + let(:entry) { described_class.new(config, global: global) } + let(:global) { spy('Global') } describe 'validations' do + before do + entry.process! + entry.validate! + end + context 'when entry config value is correct' do let(:config) { { script: 'rspec' } } describe '#value' do it 'returns key value' do - expect(entry.value).to eq(script: 'rspec') + expect(entry.value) + .to eq(script: 'rspec', + stage: 'test') end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 1ecc3e18d4e..52018958dcf 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Jobs do - let(:entry) { described_class.new(config) } + let(:entry) { described_class.new(config, global: spy) } describe 'validations' do context 'when entry config value is correct' do @@ -61,8 +61,9 @@ describe Gitlab::Ci::Config::Node::Jobs do describe '#value' do it 'returns key value' do - expect(entry.value).to eq(rspec: { script: 'rspec' }, - spinach: { script: 'spinach' }) + expect(entry.value) + .to eq(rspec: { script: 'rspec', stage: 'test' }, + spinach: { script: 'spinach', stage: 'test' }) end end diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb index 95b46d76adb..6deeca1a6c0 100644 --- a/spec/lib/gitlab/ci/config/node/stage_spec.rb +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -6,7 +6,11 @@ describe Gitlab::Ci::Config::Node::Stage do describe 'validations' do context 'when stage config value is correct' do - let(:config) { :build } + let(:config) { 'build' } + + before do + allow(global).to receive(:stages).and_return(%w[build]) + end describe '#value' do it 'returns a stage key' do @@ -39,16 +43,16 @@ describe Gitlab::Ci::Config::Node::Stage do it 'reports errors about wrong type' do expect(stage.errors) - .to include 'stage config should be a string or symbol' + .to include 'stage config should be a string' end end context 'when stage is not present in global configuration' do - let(:config) { :unknown } + let(:config) { 'unknown' } before do allow(global) - .to receive(:stages).and_return([:test, :deploy]) + .to receive(:stages).and_return(%w[test deploy]) end it 'reports error about missing stage' do @@ -65,7 +69,7 @@ describe Gitlab::Ci::Config::Node::Stage do describe '#known?' do before do - allow(global).to receive(:stages).and_return([:test, :deploy]) + allow(global).to receive(:stages).and_return(%w[test deploy]) end context 'when stage is not known' do @@ -77,7 +81,7 @@ describe Gitlab::Ci::Config::Node::Stage do end context 'when stage is known' do - let(:config) { :test } + let(:config) { 'test' } it 'returns false' do expect(stage.known?).to be true @@ -87,7 +91,7 @@ describe Gitlab::Ci::Config::Node::Stage do describe '.default' do it 'returns default stage' do - expect(described_class.default).to eq :test + expect(described_class.default).to eq 'test' end end end From 9edced40dd7398d1aa553a5454f95ae629da2276 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 9 Jul 2016 16:51:26 +0200 Subject: [PATCH 025/198] Use node factory to assemble global CI config entry --- lib/gitlab/ci/config/node/configurable.rb | 4 +-- lib/gitlab/ci/config/node/global.rb | 36 +++++++++++-------- spec/lib/gitlab/ci/config/node/global_spec.rb | 4 +-- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index e5780c60e70..7a43d494d3d 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -27,8 +27,8 @@ module Gitlab def create(key, factory) factory - .value(config[key]) - .with(key: key, parent: self, global: global) + .value(@config[key]) + .with(key: key, parent: self, global: @global) factory.create! end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index 110d982588b..a2649e2c905 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -33,30 +33,38 @@ module Gitlab node :cache, Node::Cache, description: 'Configure caching between build jobs.' - node :jobs, Node::Jobs, - description: 'Definition of jobs for this pipeline.' - helpers :before_script, :image, :services, :after_script, :variables, :stages, :types, :cache, :jobs - def initialize(config, **attributes) - super(setup(config), attributes) + def initialize(*) + super @global = self end - def stages - stages_defined? ? stages_value : types_value - end - private - def setup(config) - return config unless config.is_a?(Hash) + def compose! + super - jobs = config.except(*nodes.keys) - global = config.slice(*nodes.keys) + compose_stages! + compose_jobs! + end - global.merge(jobs: jobs) + def compose_stages! + factory = Node::Factory.new(Node::Jobs) + factory.value(@config.except(*nodes.keys)) + factory.with(key: :jobs, parent: self, global: self) + factory.with(description: 'Jobs definition for this pipeline') + + @entries[:jobs] = factory.create! + end + + def compose_jobs! + if types_defined? && !stages_defined? + @entries[:stages] = @entries[:types] + end + + @entries.delete(:types) end end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 0c56b59db0c..10e5f05a2d5 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -35,7 +35,7 @@ describe Gitlab::Ci::Config::Node::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 9 + expect(global.descendants.count).to eq 8 end it 'creates node object using valid class' do @@ -142,7 +142,7 @@ describe Gitlab::Ci::Config::Node::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 9 + expect(global.descendants.count).to eq 8 end it 'contains undefined nodes' do From e9b42067fd4065e57a68e4b37732dda91444e3f7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 9 Jul 2016 17:06:53 +0200 Subject: [PATCH 026/198] Remove CI job stage code from legacy config processor --- lib/ci/gitlab_ci_yaml_processor.rb | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index f0710690985..1ffbd0020bb 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -57,6 +57,9 @@ module Ci private def initial_parsing + ## + # Global config + # @before_script = @ci_config.before_script @image = @ci_config.image @after_script = @ci_config.after_script @@ -65,24 +68,16 @@ module Ci @stages = @ci_config.stages @cache = @ci_config.cache - @jobs = {} - - @ci_config.jobs.each do |name, param| - add_job(name, param) - end + ## + # Jobs + # + @jobs = @ci_config.jobs @jobs.each do |name, job| validate_job!(name, job) end end - def add_job(name, job) - raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) - - stage = job[:stage] || job[:type] || DEFAULT_STAGE - @jobs[name] = { stage: stage }.merge(job) - end - def build_job(name, job) { stage_idx: @stages.index(job[:stage]), @@ -112,12 +107,13 @@ module Ci end def validate_job!(name, job) + raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) + validate_job_name!(name) validate_job_keys!(name, job) validate_job_types!(name, job) validate_job_script!(name, job) - validate_job_stage!(name, job) if job[:stage] validate_job_variables!(name, job) if job[:variables] validate_job_cache!(name, job) if job[:cache] validate_job_artifacts!(name, job) if job[:artifacts] @@ -186,12 +182,6 @@ module Ci end end - def validate_job_stage!(name, job) - unless job[:stage].is_a?(String) && job[:stage].in?(@stages) - raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" - end - end - def validate_job_variables!(name, job) unless validate_variables(job[:variables]) raise ValidationError, From a80a01e8411cee9e0f7d24ddddb65dca0c7a7fdf Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 9 Jul 2016 17:38:03 +0200 Subject: [PATCH 027/198] Add comment for deprecated CI config `types` entry --- lib/ci/gitlab_ci_yaml_processor.rb | 4 +--- lib/gitlab/ci/config/node/global.rb | 11 +++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 1ffbd0020bb..40d1b475013 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -17,9 +17,7 @@ module Ci def initialize(config, path = nil) @ci_config = Gitlab::Ci::Config.new(config) - @config = @ci_config.to_hash - - @path = path + @config, @path = @ci_config.to_hash, path unless @ci_config.valid? raise ValidationError, @ci_config.errors.first diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index a2649e2c905..f59a967b1ef 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -50,7 +50,7 @@ module Gitlab compose_jobs! end - def compose_stages! + def compose_jobs! factory = Node::Factory.new(Node::Jobs) factory.value(@config.except(*nodes.keys)) factory.with(key: :jobs, parent: self, global: self) @@ -59,7 +59,14 @@ module Gitlab @entries[:jobs] = factory.create! end - def compose_jobs! + def compose_stages! + ## + # Deprecated `:types` key workaround - if types are defined and + # stages are not defined we use types definition as stages. + # + # Otherwise we use stages in favor of types, and remove types from + # processing. + # if types_defined? && !stages_defined? @entries[:stages] = @entries[:types] end From 2480701436bf84281e4afd65eb0d4c2d642754b9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sat, 9 Jul 2016 18:43:26 +0200 Subject: [PATCH 028/198] Extend CI job entries fabrication and validation --- lib/gitlab/ci/config/node/global.rb | 1 + lib/gitlab/ci/config/node/hidden_job.rb | 1 + lib/gitlab/ci/config/node/job.rb | 4 +++ lib/gitlab/ci/config/node/jobs.rb | 18 ++++++----- spec/lib/gitlab/ci/config/node/global_spec.rb | 2 +- .../gitlab/ci/config/node/hidden_job_spec.rb | 10 +++++++ spec/lib/gitlab/ci/config/node/job_spec.rb | 10 +++++++ spec/lib/gitlab/ci/config/node/jobs_spec.rb | 30 +++++++++---------- 8 files changed, 52 insertions(+), 24 deletions(-) diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index f59a967b1ef..f5500e27439 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -38,6 +38,7 @@ module Gitlab def initialize(*) super + @global = self end diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden_job.rb index 6a559ee8c04..073044b66f8 100644 --- a/lib/gitlab/ci/config/node/hidden_job.rb +++ b/lib/gitlab/ci/config/node/hidden_job.rb @@ -10,6 +10,7 @@ module Gitlab validations do validates :config, type: Hash + validates :config, presence: true end def relevant? diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 4a9cc28d763..28a3f407605 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -8,6 +8,10 @@ module Gitlab class Job < Entry include Configurable + validations do + validates :config, presence: true + end + node :stage, Stage, description: 'Pipeline stage this job will be executed into.' diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index f6acc25e4fb..7a164b69aff 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -30,17 +30,19 @@ module Gitlab private def create(name, config) - job_node(name).new(config, job_attributes(name)) + Node::Factory.new(job_node(name)) + .value(config || {}) + .with(key: name, parent: self, global: @global) + .with(description: "#{name} job definition.") + .create! end def job_node(name) - name.to_s.start_with?('.') ? Node::HiddenJob : Node::Job - end - - def job_attributes(name) - @attributes.merge(key: name, - parent: self, - description: "#{name} job definition.") + if name.to_s.start_with?('.') + Node::HiddenJob + else + Node::Job + end end end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 10e5f05a2d5..3e1c197fe61 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -137,7 +137,7 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when most of entires not defined' do - let(:hash) { { cache: { key: 'a' }, rspec: {} } } + let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } } before { global.process! } describe '#nodes' do 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 ab865c3522e..cc44e2cc054 100644 --- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb @@ -31,6 +31,16 @@ describe Gitlab::Ci::Config::Node::HiddenJob do end end end + + context 'when config is empty' do + let(:config) { {} } + + describe '#valid' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 2a4296448fb..f841936ee6b 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -39,6 +39,16 @@ describe Gitlab::Ci::Config::Node::Job do end end end + + context 'when config is empty' do + let(:config) { {} } + + describe '#valid' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 52018958dcf..b0171174157 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -4,6 +4,11 @@ describe Gitlab::Ci::Config::Node::Jobs do let(:entry) { described_class.new(config, global: spy) } describe 'validations' do + before do + entry.process! + entry.validate! + end + context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } @@ -25,25 +30,20 @@ describe Gitlab::Ci::Config::Node::Jobs do end end - context 'when no visible jobs present' do - let(:config) { { '.hidden'.to_sym => {} } } + context 'when job is unspecified' do + let(:config) { { rspec: nil } } - context 'when not processed' do - it 'is valid' do - expect(entry.errors).to be_empty - end + it 'is not valid' do + expect(entry).not_to be_valid end + end - context 'when processed' do - before do - entry.process! - entry.validate! - end + context 'when no visible jobs present' do + let(:config) { { '.hidden'.to_sym => { script: [] } } } - it 'returns error about no visible jobs defined' do - expect(entry.errors) - .to include 'jobs config should contain at least one visible job' - end + it 'returns error about no visible jobs defined' do + expect(entry.errors) + .to include 'jobs config should contain at least one visible job' end end end From 3c5b1da2a1f15be9e032ec23f56de0af8002ec6b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 10 Jul 2016 13:54:39 +0200 Subject: [PATCH 029/198] Add before_script node to CI job entry config --- lib/gitlab/ci/config/node/job.rb | 18 +++++++- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/node/job_spec.rb | 46 ++++++++++++++------ 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 28a3f407605..2f62248bb00 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -12,20 +12,34 @@ module Gitlab validates :config, presence: true end + node :before_script, Script, + description: 'Global before script overridden in this job.' + node :stage, Stage, description: 'Pipeline stage this job will be executed into.' node :type, Stage, description: 'Deprecated: stage this job will be executed into.' - helpers :stage, :type + helpers :before_script, :stage, :type def value - @config.merge(stage: stage_value) + raise InvalidError unless valid? + + ## + # TODO, refactoring step: do not expose internal configuration, + # return only hash value without merging it to internal config. + # + @config.merge(to_hash.compact) end private + def to_hash + { before_script: before_script, + stage: stage } + end + def compose! super diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 03477e1ca13..230106b74ae 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -970,7 +970,7 @@ EOT config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") end it "returns errors if after_script parameter is invalid" do diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index f841936ee6b..032fbb9c27f 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -4,23 +4,15 @@ describe Gitlab::Ci::Config::Node::Job do let(:entry) { described_class.new(config, global: global) } let(:global) { spy('Global') } - describe 'validations' do - before do - entry.process! - entry.validate! - end + before do + entry.process! + entry.validate! + end + describe 'validations' do context 'when entry config value is correct' do let(:config) { { script: 'rspec' } } - describe '#value' do - it 'returns key value' do - expect(entry.value) - .to eq(script: 'rspec', - stage: 'test') - end - end - describe '#valid?' do it 'is valid' do expect(entry).to be_valid @@ -33,7 +25,7 @@ describe Gitlab::Ci::Config::Node::Job do let(:config) { ['incorrect'] } describe '#errors' do - it 'saves errors' do + it 'reports error about a config type' do expect(entry.errors) .to include 'job config should be a hash' end @@ -52,6 +44,32 @@ describe Gitlab::Ci::Config::Node::Job do end end + describe '#value' do + context 'when entry is correct' do + let(:config) do + { before_script: %w[ls pwd], + script: 'rspec' } + end + + it 'returns correct value' do + expect(entry.value) + .to eq(before_script: %w[ls pwd], + script: 'rspec', + stage: 'test') + end + end + + context 'when entry is incorrect' do + let(:config) { {} } + + it 'raises error' do + expect { entry.value }.to raise_error( + Gitlab::Ci::Config::Node::Entry::InvalidError + ) + end + end + end + describe '#relevant?' do it 'is a relevant entry' do expect(entry).to be_relevant From 489e9be4e83621ae6f3db311398bf32e1065d1a8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 10 Jul 2016 14:35:53 +0200 Subject: [PATCH 030/198] Add CI job script node in new config processor --- lib/gitlab/ci/config/node/job.rb | 10 ++-- lib/gitlab/ci/config/node/job_script.rb | 31 ++++++++++++ spec/lib/gitlab/ci/config/node/global_spec.rb | 4 +- .../gitlab/ci/config/node/job_script_spec.rb | 49 +++++++++++++++++++ spec/lib/gitlab/ci/config/node/job_spec.rb | 2 +- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 4 +- 6 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 lib/gitlab/ci/config/node/job_script.rb create mode 100644 spec/lib/gitlab/ci/config/node/job_script_spec.rb diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 2f62248bb00..fc6fe8015d2 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -15,13 +15,16 @@ module Gitlab node :before_script, Script, description: 'Global before script overridden in this job.' + node :script, JobScript, + description: 'Commands that will be executed in this job.' + node :stage, Stage, description: 'Pipeline stage this job will be executed into.' node :type, Stage, description: 'Deprecated: stage this job will be executed into.' - helpers :before_script, :stage, :type + helpers :before_script, :script, :stage, :type def value raise InvalidError unless valid? @@ -36,8 +39,9 @@ module Gitlab private def to_hash - { before_script: before_script, - stage: stage } + { before_script: before_script_value, + script: script_value, + stage: stage_value } end def compose! diff --git a/lib/gitlab/ci/config/node/job_script.rb b/lib/gitlab/ci/config/node/job_script.rb new file mode 100644 index 00000000000..c3a05dffdd3 --- /dev/null +++ b/lib/gitlab/ci/config/node/job_script.rb @@ -0,0 +1,31 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a job script. + # + class JobScript < Entry + include Validatable + + validations do + include LegacyValidationHelpers + + validate :string_or_array_of_strings + + def string_or_array_of_strings + unless validate_string(config) || validate_array_of_strings(config) + errors.add(:config, + 'should be a string or an array of strings') + end + end + end + + def value + [@config].flatten + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 3e1c197fe61..786fc5bb6ce 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -129,8 +129,8 @@ describe Gitlab::Ci::Config::Node::Global do describe '#jobs' do it 'returns jobs configuration' do expect(global.jobs) - .to eq(rspec: { script: 'rspec', stage: 'test' }, - spinach: { script: 'spinach', stage: 'test' }) + .to eq(rspec: { script: %w[rspec], stage: 'test' }, + spinach: { script: %w[spinach], stage: 'test' }) end end end diff --git a/spec/lib/gitlab/ci/config/node/job_script_spec.rb b/spec/lib/gitlab/ci/config/node/job_script_spec.rb new file mode 100644 index 00000000000..6e2ecb6e0ad --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/job_script_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::JobScript do + let(:entry) { described_class.new(config) } + + context 'when entry config value is an array' do + let(:config) { ['ls', 'pwd'] } + + describe '#value' do + it 'returns array of strings' do + expect(entry.value).to eq config + end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + end + + context 'when entry config value is a string' do + let(:config) { 'ls' } + + describe '#value' do + it 'returns array with single element' do + expect(entry.value).to eq ['ls'] + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not valid' do + let(:config) { 1 } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'job script config should be a ' \ + 'string or an array of strings' + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 032fbb9c27f..3bcac73809d 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -54,7 +54,7 @@ describe Gitlab::Ci::Config::Node::Job do it 'returns correct value' do expect(entry.value) .to eq(before_script: %w[ls pwd], - script: 'rspec', + script: %w[rspec], stage: 'test') end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index b0171174157..255646a001a 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -62,8 +62,8 @@ describe Gitlab::Ci::Config::Node::Jobs do describe '#value' do it 'returns key value' do expect(entry.value) - .to eq(rspec: { script: 'rspec', stage: 'test' }, - spinach: { script: 'spinach', stage: 'test' }) + .to eq(rspec: { script: %w[rspec], stage: 'test' }, + spinach: { script: %w[spinach], stage: 'test' }) end end From 500b61e14f384eec545c207fa9324906daf2e148 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 10 Jul 2016 14:41:14 +0200 Subject: [PATCH 031/198] Move after script CI job confg to new processor --- lib/gitlab/ci/config/node/job.rb | 8 ++++++-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/node/job_spec.rb | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index fc6fe8015d2..050e00a5151 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -24,7 +24,10 @@ module Gitlab node :type, Stage, description: 'Deprecated: stage this job will be executed into.' - helpers :before_script, :script, :stage, :type + node :after_script, Script, + description: 'Commands that will be executed when finishing job.' + + helpers :before_script, :script, :stage, :type, :after_script def value raise InvalidError unless valid? @@ -41,7 +44,8 @@ module Gitlab def to_hash { before_script: before_script_value, script: script_value, - stage: stage_value } + stage: stage_value, + after_script: after_script_value } end def compose! diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 230106b74ae..a80b0ce5a57 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -984,7 +984,7 @@ EOT config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") end it "returns errors if image parameter is invalid" do diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 3bcac73809d..77efc73632d 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -48,14 +48,16 @@ describe Gitlab::Ci::Config::Node::Job do context 'when entry is correct' do let(:config) do { before_script: %w[ls pwd], - script: 'rspec' } + script: 'rspec', + after_script: %w[cleanup] } end it 'returns correct value' do expect(entry.value) .to eq(before_script: %w[ls pwd], script: %w[rspec], - stage: 'test') + stage: 'test', + after_script: %w[cleanup]) end end From 6aefb9e99983494b129a011ee0fce57c1398f612 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 10 Jul 2016 14:43:01 +0200 Subject: [PATCH 032/198] Remove CI job script validation from legacy config --- lib/ci/gitlab_ci_yaml_processor.rb | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 40d1b475013..ed8dd0f9e47 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -110,7 +110,6 @@ module Ci validate_job_name!(name) validate_job_keys!(name, job) validate_job_types!(name, job) - validate_job_script!(name, job) validate_job_variables!(name, job) if job[:variables] validate_job_cache!(name, job) if job[:cache] @@ -166,20 +165,6 @@ module Ci end end - def validate_job_script!(name, job) - if !validate_string(job[:script]) && !validate_array_of_strings(job[:script]) - raise ValidationError, "#{name} job: script should be a string or an array of a strings" - end - - if job[:before_script] && !validate_array_of_strings(job[:before_script]) - raise ValidationError, "#{name} job: before_script should be an array of strings" - end - - if job[:after_script] && !validate_array_of_strings(job[:after_script]) - raise ValidationError, "#{name} job: after_script should be an array of strings" - end - end - def validate_job_variables!(name, job) unless validate_variables(job[:variables]) raise ValidationError, From 8f7c98ee2a34cb063428bea81f1420579549a1a5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 10 Jul 2016 20:26:37 +0200 Subject: [PATCH 033/198] Rename CI config job script entry node to commands --- lib/gitlab/ci/config/node/{job_script.rb => commands.rb} | 8 ++++++-- lib/gitlab/ci/config/node/job.rb | 2 +- .../config/node/{job_script_spec.rb => commands_spec.rb} | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) rename lib/gitlab/ci/config/node/{job_script.rb => commands.rb} (75%) rename spec/lib/gitlab/ci/config/node/{job_script_spec.rb => commands_spec.rb} (90%) diff --git a/lib/gitlab/ci/config/node/job_script.rb b/lib/gitlab/ci/config/node/commands.rb similarity index 75% rename from lib/gitlab/ci/config/node/job_script.rb rename to lib/gitlab/ci/config/node/commands.rb index c3a05dffdd3..f7e6950001e 100644 --- a/lib/gitlab/ci/config/node/job_script.rb +++ b/lib/gitlab/ci/config/node/commands.rb @@ -5,7 +5,7 @@ module Gitlab ## # Entry that represents a job script. # - class JobScript < Entry + class Commands < Entry include Validatable validations do @@ -14,11 +14,15 @@ module Gitlab validate :string_or_array_of_strings def string_or_array_of_strings - unless validate_string(config) || validate_array_of_strings(config) + unless config_valid? errors.add(:config, 'should be a string or an array of strings') end end + + def config_valid? + validate_string(config) || validate_array_of_strings(config) + end end def value diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 050e00a5151..9a019216ca3 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -15,7 +15,7 @@ module Gitlab node :before_script, Script, description: 'Global before script overridden in this job.' - node :script, JobScript, + node :script, Commands, description: 'Commands that will be executed in this job.' node :stage, Stage, diff --git a/spec/lib/gitlab/ci/config/node/job_script_spec.rb b/spec/lib/gitlab/ci/config/node/commands_spec.rb similarity index 90% rename from spec/lib/gitlab/ci/config/node/job_script_spec.rb rename to spec/lib/gitlab/ci/config/node/commands_spec.rb index 6e2ecb6e0ad..e373c40706f 100644 --- a/spec/lib/gitlab/ci/config/node/job_script_spec.rb +++ b/spec/lib/gitlab/ci/config/node/commands_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::JobScript do +describe Gitlab::Ci::Config::Node::Commands do let(:entry) { described_class.new(config) } context 'when entry config value is an array' do @@ -41,7 +41,7 @@ describe Gitlab::Ci::Config::Node::JobScript do describe '#errors' do it 'saves errors' do expect(entry.errors) - .to include 'job script config should be a ' \ + .to include 'commands config should be a ' \ 'string or an array of strings' end end From 80587064eb798f5bf2b18dbb8e65e5a55d1db085 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Sun, 10 Jul 2016 20:59:18 +0200 Subject: [PATCH 034/198] Require parent when using node factory in CI config --- lib/gitlab/ci/config/node/configurable.rb | 9 ++-- lib/gitlab/ci/config/node/factory.rb | 13 +++++- lib/gitlab/ci/config/node/global.rb | 7 +-- lib/gitlab/ci/config/node/jobs.rb | 8 ++-- .../lib/gitlab/ci/config/node/factory_spec.rb | 43 ++++++++++++++++--- 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 7a43d494d3d..36d7d20c110 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -28,7 +28,8 @@ module Gitlab def create(key, factory) factory .value(@config[key]) - .with(key: key, parent: self, global: @global) + .parent(self) + .with(key: key) factory.create! end @@ -40,11 +41,11 @@ module Gitlab private - def node(symbol, entry_class, metadata) - factory = Node::Factory.new(entry_class) + def node(key, node, metadata) + factory = Node::Factory.new(node) .with(description: metadata[:description]) - (@nodes ||= {}).merge!(symbol.to_sym => factory) + (@nodes ||= {}).merge!(key.to_sym => factory) end def helpers(*nodes) diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 3f2cdf436e3..3488aec4a22 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -18,6 +18,11 @@ module Gitlab self end + def parent(parent) + @parent = parent + self + end + def with(attributes) @attributes.merge!(attributes) self @@ -25,15 +30,19 @@ module Gitlab def create! raise InvalidFactory unless defined?(@value) + raise InvalidFactory unless defined?(@parent) + + attributes = { parent: @parent, global: @parent.global } + attributes.merge!(@attributes) ## # We assume that unspecified entry is undefined. # See issue #18775. # if @value.nil? - Node::Undefined.new(@node, @attributes) + Node::Undefined.new(@node, attributes) else - @node.new(@value, @attributes) + @node.new(@value, attributes) end end end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index f5500e27439..4a958735599 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -53,9 +53,10 @@ module Gitlab def compose_jobs! factory = Node::Factory.new(Node::Jobs) - factory.value(@config.except(*nodes.keys)) - factory.with(key: :jobs, parent: self, global: self) - factory.with(description: 'Jobs definition for this pipeline') + .value(@config.except(*nodes.keys)) + .parent(self) + .with(key: :jobs, global: self) + .with(description: 'Jobs definition for this pipeline') @entries[:jobs] = factory.create! end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 7a164b69aff..548441df37c 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -30,14 +30,14 @@ module Gitlab private def create(name, config) - Node::Factory.new(job_node(name)) + Node::Factory.new(node(name)) .value(config || {}) - .with(key: name, parent: self, global: @global) - .with(description: "#{name} job definition.") + .parent(self) + .with(key: name, description: "#{name} job definition.") .create! end - def job_node(name) + def node(name) if name.to_s.start_with?('.') Node::HiddenJob else diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index c912b1b2044..bc6bf32ffbf 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -2,22 +2,40 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Factory do describe '#create!' do - let(:factory) { described_class.new(entry_class) } - let(:entry_class) { Gitlab::Ci::Config::Node::Script } + let(:factory) { described_class.new(node) } + let(:node) { Gitlab::Ci::Config::Node::Script } + let(:parent) { double('parent') } + let(:global) { double('global') } - context 'when setting up a value' do + before do + allow(parent).to receive(:global).and_return(global) + end + + context 'when setting a concrete value' do it 'creates entry with valid value' do entry = factory .value(['ls', 'pwd']) + .parent(parent) .create! expect(entry.value).to eq ['ls', 'pwd'] end + it 'sets parent and global attributes' do + entry = factory + .value('ls') + .parent(parent) + .create! + + expect(entry.global).to eq global + expect(entry.parent).to eq parent + end + context 'when setting description' do it 'creates entry with description' do entry = factory .value(['ls', 'pwd']) + .parent(parent) .with(description: 'test description') .create! @@ -30,6 +48,7 @@ describe Gitlab::Ci::Config::Node::Factory do it 'creates entry with custom key' do entry = factory .value(['ls', 'pwd']) + .parent(parent) .with(key: 'test key') .create! @@ -38,20 +57,21 @@ describe Gitlab::Ci::Config::Node::Factory do end context 'when setting a parent' do - let(:parent) { Object.new } + let(:object) { Object.new } it 'creates entry with valid parent' do entry = factory .value('ls') - .with(parent: parent) + .parent(parent) + .with(parent: object) .create! - expect(entry.parent).to eq parent + expect(entry.parent).to eq object end end end - context 'when not setting up a value' do + context 'when not setting a value' do it 'raises error' do expect { factory.create! }.to raise_error( Gitlab::Ci::Config::Node::Factory::InvalidFactory @@ -59,10 +79,19 @@ describe Gitlab::Ci::Config::Node::Factory do end end + context 'when not setting parent object' do + it 'raises error' do + expect { factory.value('ls').create! }.to raise_error( + Gitlab::Ci::Config::Node::Factory::InvalidFactory + ) + end + end + context 'when creating entry with nil value' do it 'creates an undefined entry' do entry = factory .value(nil) + .parent(parent) .create! expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined From 8c9c3eda7a305c43d7cf3d50b868292d0b612b5b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 12 Jul 2016 12:56:21 +0200 Subject: [PATCH 035/198] Prevalidate CI entries recursively on processed --- lib/gitlab/ci/config/node/entry.rb | 5 ++--- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 1940c39087b..02e4f7693da 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -31,10 +31,9 @@ module Gitlab end def validate! - if @validator.valid?(:new) - @validator.validate(:processed) - end + return unless valid? + @validator.validate(:processed) @entries.each_value(&:validate!) end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index a80b0ce5a57..1017f79cc6e 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1065,7 +1065,7 @@ EOT end it "returns errors if there are no visible jobs defined" do - config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => {} }) + config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) expect do GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") From d41d3301474ffd7022e41daad4ddf67590ac9f95 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 12 Jul 2016 13:03:19 +0200 Subject: [PATCH 036/198] Add CI config node that is unspecified null entry --- lib/gitlab/ci/config/node/null.rb | 30 +++++++++++++++++++++ spec/lib/gitlab/ci/config/node/null_spec.rb | 29 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 lib/gitlab/ci/config/node/null.rb create mode 100644 spec/lib/gitlab/ci/config/node/null_spec.rb diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb new file mode 100644 index 00000000000..c7bbc16939a --- /dev/null +++ b/lib/gitlab/ci/config/node/null.rb @@ -0,0 +1,30 @@ +module Gitlab + module Ci + class Config + module Node + ## + # This class represents an undefined and unspecified node. + # + # Implements the Null Object pattern. + # + class Null < Entry + def initialize(config = nil, **attributes) + super + end + + def value + nil + end + + def valid? + true + end + + def errors + [] + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb new file mode 100644 index 00000000000..63b23ed0bd1 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/null_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Null do + let(:null) { described_class.new } + + describe '#leaf?' do + it 'is leaf node' do + expect(null).to be_leaf + end + end + + describe '#valid?' do + it 'is always valid' do + expect(null).to be_valid + end + end + + describe '#errors' do + it 'is does not contain errors' do + expect(null.errors).to be_empty + end + end + + describe '#value' do + it 'returns nil' do + expect(null.value).to eq nil + end + end +end From 06641a3fee4ebdaada3007a51866a7fb927d21de Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 12 Jul 2016 14:10:18 +0200 Subject: [PATCH 037/198] Simplify undefined node definition in CI config --- lib/gitlab/ci/config/node/configurable.rb | 2 +- lib/gitlab/ci/config/node/entry.rb | 4 +- lib/gitlab/ci/config/node/factory.rb | 12 +++- lib/gitlab/ci/config/node/null.rb | 12 ++-- lib/gitlab/ci/config/node/undefined.rb | 59 ++------------- spec/lib/gitlab/ci/config/node/global_spec.rb | 4 +- spec/lib/gitlab/ci/config/node/null_spec.rb | 14 +++- .../gitlab/ci/config/node/undefined_spec.rb | 71 +++++-------------- 8 files changed, 60 insertions(+), 118 deletions(-) diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 36d7d20c110..29de2d7d0b5 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -51,7 +51,7 @@ module Gitlab def helpers(*nodes) nodes.each do |symbol| define_method("#{symbol}_defined?") do - @entries[symbol].try(:defined?) + @entries[symbol].specified? end define_method("#{symbol}_value") do diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 02e4f7693da..c95e85200d2 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -66,14 +66,14 @@ module Gitlab @config else meaningful = @entries.select do |_key, value| - value.defined? && value.relevant? + value.specified? && value.relevant? end Hash[meaningful.map { |key, entry| [key, entry.value] }] end end - def defined? + def specified? true end diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 3488aec4a22..602660acb93 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -40,11 +40,21 @@ module Gitlab # See issue #18775. # if @value.nil? - Node::Undefined.new(@node, attributes) + Node::Undefined.new(fabricate_undefined(attributes)) else @node.new(@value, attributes) end end + + private + + def fabricate_undefined(attributes) + if @node.default.nil? + Node::Null.new(nil, attributes) + else + @node.new(@node.default, attributes) + end + end end end end diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb index c7bbc16939a..880d29f663d 100644 --- a/lib/gitlab/ci/config/node/null.rb +++ b/lib/gitlab/ci/config/node/null.rb @@ -8,10 +8,6 @@ module Gitlab # Implements the Null Object pattern. # class Null < Entry - def initialize(config = nil, **attributes) - super - end - def value nil end @@ -23,6 +19,14 @@ module Gitlab def errors [] end + + def specified? + false + end + + def relevant? + false + end end end end diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index fedb9d020be..384774c9b69 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -3,66 +3,19 @@ module Gitlab class Config module Node ## - # This class represents an undefined entry node. + # This class represents an undefined and unspecified entry node. # - # It takes original entry class as configuration and creates an object - # if original entry has a default value. If there is default value - # some methods are delegated to it. + # It decorates original entry adding method that idicates it is + # unspecified. # - # - class Undefined < Entry - include Validatable - - delegate :valid?, :errors, :value, to: :@strategy - - validations do - validates :config, type: Class - end - - def initialize(node, **attributes) + class Undefined < SimpleDelegator + def initialize(entry) super - @strategy = create_strategy(node, node.default) end - def defined? + def specified? false end - - private - - def create_strategy(node, default) - if default.nil? - Undefined::NullStrategy.new - else - entry = node.new(default, attributes) - Undefined::DefaultStrategy.new(entry) - end - end - - class DefaultStrategy - delegate :valid?, :errors, :value, to: :@default - - def initialize(entry) - @default = entry - end - end - - class NullStrategy - def initialize(*) - end - - def value - nil - end - - def valid? - true - end - - def errors - [] - end - end end end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 786fc5bb6ce..1945b0326cc 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -233,9 +233,9 @@ describe Gitlab::Ci::Config::Node::Global do end end - describe '#defined?' do + describe '#specified?' do it 'is concrete entry that is defined' do - expect(global.defined?).to be true + expect(global.specified?).to be true end end end diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb index 63b23ed0bd1..1ab5478dcfa 100644 --- a/spec/lib/gitlab/ci/config/node/null_spec.rb +++ b/spec/lib/gitlab/ci/config/node/null_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Null do - let(:null) { described_class.new } + let(:null) { described_class.new(nil) } describe '#leaf?' do it 'is leaf node' do @@ -26,4 +26,16 @@ describe Gitlab::Ci::Config::Node::Null do expect(null.value).to eq nil end end + + describe '#relevant?' do + it 'is not relevant' do + expect(null.relevant?).to eq false + end + end + + describe '#specified?' do + it 'is not defined' do + expect(null.specified?).to eq false + end + end end diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb index 417b4a0ad6f..2d43e1c1a9d 100644 --- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb +++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb @@ -4,66 +4,29 @@ describe Gitlab::Ci::Config::Node::Undefined do let(:undefined) { described_class.new(entry) } let(:entry) { spy('Entry') } - context 'when entry does not have a default value' do - before { allow(entry).to receive(:default).and_return(nil) } - - describe '#leaf?' do - it 'is leaf node' do - expect(undefined).to be_leaf - end - end - - describe '#valid?' do - it 'is always valid' do - expect(undefined).to be_valid - end - end - - describe '#errors' do - it 'is does not contain errors' do - expect(undefined.errors).to be_empty - end - end - - describe '#value' do - it 'returns nil' do - expect(undefined.value).to eq nil - end + describe '#valid?' do + it 'delegates method to entry' do + expect(undefined.valid).to eq entry end end - context 'when entry has a default value' do - before do - allow(entry).to receive(:default).and_return('some value') - allow(entry).to receive(:value).and_return('some value') - end - - describe '#value' do - it 'returns default value for entry' do - expect(undefined.value).to eq 'some value' - end - end - - describe '#errors' do - it 'delegates errors to default entry' do - expect(entry).to receive(:errors) - - undefined.errors - end - end - - describe '#valid?' do - it 'delegates valid? to default entry' do - expect(entry).to receive(:valid?) - - undefined.valid? - end + describe '#errors' do + it 'delegates method to entry' do + expect(undefined.errors).to eq entry end end - describe '#undefined?' do - it 'is not a defined entry' do - expect(undefined.defined?).to be false + describe '#value' do + it 'delegates method to entry' do + expect(undefined.value).to eq entry + end + end + + describe '#specified?' do + it 'is always false' do + allow(entry).to receive(:specified?).and_return(true) + + expect(undefined.specified?).to be false end end end From 61f7bede79a006c7b44e88a3385d175c5ad2a863 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 12 Jul 2016 14:40:51 +0200 Subject: [PATCH 038/198] Fix using `try` on delegators in CI config entries See: https://github.com/rails/rails/commit/af53280a4b5b3323ac87dc60deb2b1b781197b2b --- lib/gitlab/ci/config/node/configurable.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 29de2d7d0b5..da2ef4d5503 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -51,12 +51,12 @@ module Gitlab def helpers(*nodes) nodes.each do |symbol| define_method("#{symbol}_defined?") do - @entries[symbol].specified? + @entries[symbol].specified? if @entries[symbol] end define_method("#{symbol}_value") do raise Entry::InvalidError unless valid? - @entries[symbol].try(:value) + @entries[symbol].value if @entries[symbol] end alias_method symbol.to_sym, "#{symbol}_value".to_sym From b228787f5afa34b153e6b52d6b0d88248cc3e099 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 12 Jul 2016 14:58:48 +0200 Subject: [PATCH 039/198] Do not raise when getting value of invalid CI node --- lib/gitlab/ci/config/node/configurable.rb | 2 +- spec/lib/gitlab/ci/config/node/global_spec.rb | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index da2ef4d5503..8bd752b0e2a 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -55,7 +55,7 @@ module Gitlab end define_method("#{symbol}_value") do - raise Entry::InvalidError unless valid? + return unless valid? @entries[symbol].value if @entries[symbol] end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 1945b0326cc..3ffbe9c2e97 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -209,10 +209,8 @@ describe Gitlab::Ci::Config::Node::Global do end describe '#before_script' do - it 'raises error' do - expect { global.before_script }.to raise_error( - Gitlab::Ci::Config::Node::Entry::InvalidError - ) + it 'returns nil' do + expect(global.before_script).to be_nil end end end From de4c9a273867864a9033dab0624e0cfd72201384 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 13 Jul 2016 12:22:33 +0200 Subject: [PATCH 040/198] Improve CI stage configuration entry validations --- lib/gitlab/ci/config/node/stage.rb | 16 +++++++++------- lib/gitlab/ci/config/node/validators.rb | 2 +- spec/lib/gitlab/ci/config/node/stage_spec.rb | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb index e8fae65a2a9..909358ea170 100644 --- a/lib/gitlab/ci/config/node/stage.rb +++ b/lib/gitlab/ci/config/node/stage.rb @@ -10,14 +10,16 @@ module Gitlab validations do validates :config, type: String - validates :global, required_attribute: true - validate :known_stage, on: :processed - def known_stage - unless known? - stages_list = global.stages.join(', ') - errors.add(:config, - "should be one of defined stages (#{stages_list})") + with_options on: :processed do + validates :global, required: true + + validate do + unless known? + errors.add(:config, + 'should be one of defined stages ' \ + "(#{global.stages.join(', ')})") + end end end end diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index 6f0e14e2f0a..d33b407af68 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -33,7 +33,7 @@ module Gitlab end end - class RequiredAttributeValidator < ActiveModel::EachValidator + class RequiredValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) if value.nil? raise Entry::InvalidError, diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb index 6deeca1a6c0..004012f8b38 100644 --- a/spec/lib/gitlab/ci/config/node/stage_spec.rb +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -28,10 +28,10 @@ describe Gitlab::Ci::Config::Node::Stage do context 'when stage config is incorrect' do describe '#errors' do context 'when reference to global node is not set' do - let(:stage) { described_class.new(config) } + let(:stage) { described_class.new('test') } it 'raises error' do - expect { stage }.to raise_error( + expect { stage.validate! }.to raise_error( Gitlab::Ci::Config::Node::Entry::InvalidError, /Entry needs global attribute set internally./ ) From 097550f08a2a92dd1efff5da97cff0228afde57b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 13 Jul 2016 13:42:57 +0200 Subject: [PATCH 041/198] Fabricate CI entry with value, set attributes later --- lib/gitlab/ci/config/node/factory.rb | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 602660acb93..339548e0feb 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -32,27 +32,39 @@ module Gitlab raise InvalidFactory unless defined?(@value) raise InvalidFactory unless defined?(@parent) - attributes = { parent: @parent, global: @parent.global } - attributes.merge!(@attributes) - ## # We assume that unspecified entry is undefined. # See issue #18775. # if @value.nil? - Node::Undefined.new(fabricate_undefined(attributes)) + Node::Undefined.new( + fabricate_undefined + ) else - @node.new(@value, attributes) + fabricate(@node, @value) end end private - def fabricate_undefined(attributes) + def fabricate_undefined + ## + # If node has a default value we fabricate concrete node + # with default value. + # if @node.default.nil? - Node::Null.new(nil, attributes) + fabricate(Node::Null) else - @node.new(@node.default, attributes) + fabricate(@node, @node.default) + end + end + + def fabricate(node, value = nil) + node.new(value).tap do |entry| + entry.key = @attributes[:key] + entry.parent = @attributes[:parent] || @parent + entry.global = @attributes[:global] || @parent.global + entry.description = @attributes[:description] end end end From 6920390aad683dcc73109be5a23b647c918f9309 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 13 Jul 2016 14:38:10 +0200 Subject: [PATCH 042/198] Add before script and commands to CI job entry --- lib/gitlab/ci/config/node/job.rb | 15 ++- spec/lib/gitlab/ci/config/node/job_spec.rb | 117 +++++++++++++++++++-- 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 9a019216ca3..5ee91ebcf0b 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -10,6 +10,7 @@ module Gitlab validations do validates :config, presence: true + validates :global, required: true, on: :processed end node :before_script, Script, @@ -30,8 +31,6 @@ module Gitlab helpers :before_script, :script, :stage, :type, :after_script def value - raise InvalidError unless valid? - ## # TODO, refactoring step: do not expose internal configuration, # return only hash value without merging it to internal config. @@ -39,6 +38,18 @@ module Gitlab @config.merge(to_hash.compact) end + def before_script + if before_script_defined? + before_script_value.to_a + else + @global.before_script.to_a + end + end + + def commands + (before_script + script).join("\n") + end + private def to_hash diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 77efc73632d..635362611a0 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -60,14 +60,62 @@ describe Gitlab::Ci::Config::Node::Job do after_script: %w[cleanup]) end end + end - context 'when entry is incorrect' do - let(:config) { {} } + describe '#before_script' do + context 'when global entry has before script' do + before do + allow(global).to receive(:before_script) + .and_return(%w[ls pwd]) + end - it 'raises error' do - expect { entry.value }.to raise_error( - Gitlab::Ci::Config::Node::Entry::InvalidError - ) + context 'when before script is overridden' do + let(:config) do + { before_script: %w[whoami], + script: 'rspec' } + end + + it 'returns correct script' do + expect(entry.before_script).to eq %w[whoami] + end + end + + context 'when before script is not overriden' do + let(:config) do + { script: %w[spinach] } + end + + it 'returns correct script' do + expect(entry.before_script).to eq %w[ls pwd] + end + end + end + + context 'when global entry does not have before script' do + before do + allow(global).to receive(:before_script) + .and_return(nil) + end + + context 'when job has before script' do + let(:config) do + { before_script: %w[whoami], + script: 'rspec' } + end + + it 'returns correct script' do + expect(entry.before_script).to eq %w[whoami] + end + end + + context 'when job does not have before script' do + let(:config) do + { script: %w[ls test] } + end + + it 'returns correct script' do + expect(entry.before_script).to eq [] + end end end end @@ -77,4 +125,61 @@ describe Gitlab::Ci::Config::Node::Job do expect(entry).to be_relevant end end + + describe '#commands' do + context 'when global entry has before script' do + before do + allow(global).to receive(:before_script) + .and_return(%w[ls pwd]) + end + + context 'when before script is overridden' do + let(:config) do + { before_script: %w[whoami], + script: 'rspec' } + end + + it 'returns correct commands' do + expect(entry.commands).to eq "whoami\nrspec" + end + end + + context 'when before script is not overriden' do + let(:config) do + { script: %w[rspec spinach] } + end + + it 'returns correct commands' do + expect(entry.commands).to eq "ls\npwd\nrspec\nspinach" + end + end + end + + context 'when global entry does not have before script' do + before do + allow(global).to receive(:before_script) + .and_return(nil) + end + context 'when job has before script' do + let(:config) do + { before_script: %w[whoami], + script: 'rspec' } + end + + it 'returns correct commands' do + expect(entry.commands).to eq "whoami\nrspec" + end + end + + context 'when job does not have before script' do + let(:config) do + { script: %w[ls test] } + end + + it 'returns correct commands' do + expect(entry.commands).to eq "ls\ntest" + end + end + end + end end From 036e297ca3c39f90aebc76d5acb2e01f32364d0d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 13 Jul 2016 15:04:12 +0200 Subject: [PATCH 043/198] Expose CI job commands and use in legacy processor --- lib/ci/gitlab_ci_yaml_processor.rb | 13 ++++++------- lib/gitlab/ci/config/node/job.rb | 15 ++++++++------- spec/lib/gitlab/ci/config/node/global_spec.rb | 12 +++++++++--- spec/lib/gitlab/ci/config/node/job_spec.rb | 3 ++- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 11 ++++++++--- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index ed8dd0f9e47..61075d3b923 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -80,12 +80,7 @@ module Ci { stage_idx: @stages.index(job[:stage]), stage: job[:stage], - ## - # Refactoring note: - # - before script behaves differently than after script - # - after script returns an array of commands - # - before script should be a concatenated command - commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"), + commands: job[:commands], tag_list: job[:tags] || [], name: name, only: job[:only], @@ -124,8 +119,12 @@ module Ci end def validate_job_keys!(name, job) + ## + # TODO, remove refactoring keys + # + refactoring_keys = [:commands] job.keys.each do |key| - unless ALLOWED_JOB_KEYS.include? key + unless (ALLOWED_JOB_KEYS + refactoring_keys).include? key raise ValidationError, "#{name} job: unknown parameter #{key}" end end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 5ee91ebcf0b..bb1c3386bd4 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -40,23 +40,24 @@ module Gitlab def before_script if before_script_defined? - before_script_value.to_a + before_script_value else - @global.before_script.to_a + @global.before_script end end def commands - (before_script + script).join("\n") + [before_script, script].compact.join("\n") end private def to_hash - { before_script: before_script_value, - script: script_value, - stage: stage_value, - after_script: after_script_value } + { before_script: before_script, + script: script, + commands: commands, + stage: stage, + after_script: after_script } end def compose! diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 3ffbe9c2e97..f46359f7ee6 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::Node::Global do after_script: ['make clean'], stages: ['build', 'pages'], cache: { key: 'k', untracked: true, paths: ['public/'] }, - rspec: { script: 'rspec' }, + rspec: { script: %w[rspec ls] }, spinach: { script: 'spinach' } } end @@ -129,8 +129,14 @@ describe Gitlab::Ci::Config::Node::Global do describe '#jobs' do it 'returns jobs configuration' do expect(global.jobs) - .to eq(rspec: { script: %w[rspec], stage: 'test' }, - spinach: { script: %w[spinach], stage: 'test' }) + .to eq(rspec: { before_script: %w[ls pwd], + script: %w[rspec ls], + commands: "ls\npwd\nrspec\nls", + stage: 'test' }, + spinach: { before_script: %w[ls pwd], + script: %w[spinach], + commands: "ls\npwd\nspinach", + stage: 'test' }) end end end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 635362611a0..816c0f275d6 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -56,6 +56,7 @@ describe Gitlab::Ci::Config::Node::Job do expect(entry.value) .to eq(before_script: %w[ls pwd], script: %w[rspec], + commands: "ls\npwd\nrspec", stage: 'test', after_script: %w[cleanup]) end @@ -114,7 +115,7 @@ describe Gitlab::Ci::Config::Node::Job do end it 'returns correct script' do - expect(entry.before_script).to eq [] + expect(entry.before_script).to be_nil end end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 255646a001a..60ab1d2150d 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Jobs do - let(:entry) { described_class.new(config, global: spy) } + let(:entry) { described_class.new(config, global: global) } + let(:global) { double('global', before_script: nil, stages: %w[test]) } describe 'validations' do before do @@ -62,8 +63,12 @@ describe Gitlab::Ci::Config::Node::Jobs do describe '#value' do it 'returns key value' do expect(entry.value) - .to eq(rspec: { script: %w[rspec], stage: 'test' }, - spinach: { script: %w[spinach], stage: 'test' }) + .to eq(rspec: { script: %w[rspec], + commands: 'rspec', + stage: 'test' }, + spinach: { script: %w[spinach], + commands: 'spinach', + stage: 'test' }) end end From 56ae9f6ba933939ced7c8e0eea5abbb34a0a68be Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 14 Jul 2016 13:14:09 +0200 Subject: [PATCH 044/198] Improve CI job entry validations in new config --- lib/ci/gitlab_ci_yaml_processor.rb | 7 ------- lib/gitlab/ci/config/node/configurable.rb | 6 ++++-- lib/gitlab/ci/config/node/job.rb | 20 +++++++++++-------- lib/gitlab/ci/config/node/jobs.rb | 11 +++++----- lib/gitlab/ci/config/node/validator.rb | 11 ++++++++-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 ++-- spec/lib/gitlab/ci/config/node/global_spec.rb | 14 ++++++------- spec/lib/gitlab/ci/config/node/job_spec.rb | 16 ++++++++++++--- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 4 ++-- 9 files changed, 54 insertions(+), 39 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 61075d3b923..e18d5b907b4 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -102,7 +102,6 @@ module Ci def validate_job!(name, job) raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) - validate_job_name!(name) validate_job_keys!(name, job) validate_job_types!(name, job) @@ -112,12 +111,6 @@ module Ci validate_job_dependencies!(name, job) if job[:dependencies] end - def validate_job_name!(name) - if name.blank? || !validate_string(name) - raise ValidationError, "job name should be non-empty string" - end - end - def validate_job_keys!(name, job) ## # TODO, remove refactoring keys diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 8bd752b0e2a..b33d743e2c3 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -55,8 +55,10 @@ module Gitlab end define_method("#{symbol}_value") do - return unless valid? - @entries[symbol].value if @entries[symbol] + if @entries[symbol] + return unless @entries[symbol].valid? + @entries[symbol].value + end end alias_method symbol.to_sym, "#{symbol}_value".to_sym diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index bb1c3386bd4..822b428926a 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -10,7 +10,12 @@ module Gitlab validations do validates :config, presence: true - validates :global, required: true, on: :processed + + with_options on: :processed do + validates :global, required: true + validates :name, presence: true + validates :name, type: Symbol + end end node :before_script, Script, @@ -30,11 +35,11 @@ module Gitlab helpers :before_script, :script, :stage, :type, :after_script + def name + @key + end + def value - ## - # TODO, refactoring step: do not expose internal configuration, - # return only hash value without merging it to internal config. - # @config.merge(to_hash.compact) end @@ -53,10 +58,9 @@ module Gitlab private def to_hash - { before_script: before_script, - script: script, - commands: commands, + { script: script, stage: stage, + commands: commands, after_script: after_script } end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 548441df37c..3c1851b9fea 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -10,11 +10,12 @@ module Gitlab validations do validates :config, type: Hash - validate :jobs_presence, on: :processed - def jobs_presence - unless relevant? - errors.add(:config, 'should contain at least one visible job') + with_options on: :processed do + validate do + unless has_visible_job? + errors.add(:config, 'should contain at least one visible job') + end end end end @@ -23,7 +24,7 @@ module Gitlab @config end - def relevant? + def has_visible_job? @entries.values.any?(&:relevant?) end diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb index dcfeb194374..ca000f245aa 100644 --- a/lib/gitlab/ci/config/node/validator.rb +++ b/lib/gitlab/ci/config/node/validator.rb @@ -31,8 +31,15 @@ module Gitlab def location predecessors = ancestors.map(&:key).compact - current = key || @node.class.name.demodulize.underscore.humanize - predecessors.append(current).join(':') + predecessors.append(key_name).join(':') + end + + def key_name + if key.blank? || key.nil? + @node.class.name.demodulize.underscore.humanize + else + key + end end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 1017f79cc6e..e88f5cfc6dd 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -998,14 +998,14 @@ EOT config = YAML.dump({ '' => { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank") end it "returns errors if job name is non-string" do config = YAML.dump({ 10 => { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol") end it "returns errors if job image parameter is invalid" do diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index f46359f7ee6..fa5ff016995 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -129,14 +129,12 @@ describe Gitlab::Ci::Config::Node::Global do describe '#jobs' do it 'returns jobs configuration' do expect(global.jobs) - .to eq(rspec: { before_script: %w[ls pwd], - script: %w[rspec ls], - commands: "ls\npwd\nrspec\nls", - stage: 'test' }, - spinach: { before_script: %w[ls pwd], - script: %w[spinach], - commands: "ls\npwd\nspinach", - stage: 'test' }) + .to eq(rspec: { script: %w[rspec ls], + stage: 'test', + commands: "ls\npwd\nrspec\nls" }, + spinach: { script: %w[spinach], + stage: 'test', + commands: "ls\npwd\nspinach" }) end end end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 816c0f275d6..2ac7509cb6d 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Job do - let(:entry) { described_class.new(config, global: global) } - let(:global) { spy('Global') } + let(:entry) { described_class.new(config, attributes) } + let(:attributes) { { key: :rspec, global: global } } + let(:global) { double('global', stages: %w[test]) } before do entry.process! @@ -18,6 +19,15 @@ describe Gitlab::Ci::Config::Node::Job do expect(entry).to be_valid end end + + context 'when job name is empty' do + let(:attributes) { { key: '', global: global } } + + it 'reports error' do + expect(entry.errors) + .to include "job name can't be blank" + end + end end context 'when entry value is not correct' do @@ -27,7 +37,7 @@ describe Gitlab::Ci::Config::Node::Job do describe '#errors' do it 'reports error about a config type' do expect(entry.errors) - .to include 'job config should be a hash' + .to include 'rspec config should be a hash' end end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 60ab1d2150d..fe7ae61ed81 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -34,8 +34,8 @@ describe Gitlab::Ci::Config::Node::Jobs do context 'when job is unspecified' do let(:config) { { rspec: nil } } - it 'is not valid' do - expect(entry).not_to be_valid + it 'reports error' do + expect(entry.errors).to include "rspec config can't be blank" end end From f7c80e9f31944c0001c9bef23d1a8efe33e4adce Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 14 Jul 2016 15:05:46 +0200 Subject: [PATCH 045/198] Revert references to global node in CI job entry --- lib/ci/gitlab_ci_yaml_processor.rb | 8 +- lib/gitlab/ci/config/node/job.rb | 20 +-- spec/lib/gitlab/ci/config/node/global_spec.rb | 6 +- spec/lib/gitlab/ci/config/node/job_spec.rb | 116 ------------------ spec/lib/gitlab/ci/config/node/jobs_spec.rb | 2 - 5 files changed, 8 insertions(+), 144 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index e18d5b907b4..144f9cd7b74 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -80,7 +80,7 @@ module Ci { stage_idx: @stages.index(job[:stage]), stage: job[:stage], - commands: job[:commands], + commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"), tag_list: job[:tags] || [], name: name, only: job[:only], @@ -112,12 +112,8 @@ module Ci end def validate_job_keys!(name, job) - ## - # TODO, remove refactoring keys - # - refactoring_keys = [:commands] job.keys.each do |key| - unless (ALLOWED_JOB_KEYS + refactoring_keys).include? key + unless ALLOWED_JOB_KEYS.include? key raise ValidationError, "#{name} job: unknown parameter #{key}" end end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 822b428926a..f01a46a8ddc 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -43,25 +43,13 @@ module Gitlab @config.merge(to_hash.compact) end - def before_script - if before_script_defined? - before_script_value - else - @global.before_script - end - end - - def commands - [before_script, script].compact.join("\n") - end - private def to_hash - { script: script, - stage: stage, - commands: commands, - after_script: after_script } + { before_script: before_script_value, + script: script_value, + stage: stage_value, + after_script: after_script_value } end def compose! diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index fa5ff016995..dfcaebe35be 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -130,11 +130,9 @@ describe Gitlab::Ci::Config::Node::Global do it 'returns jobs configuration' do expect(global.jobs) .to eq(rspec: { script: %w[rspec ls], - stage: 'test', - commands: "ls\npwd\nrspec\nls" }, + stage: 'test' }, spinach: { script: %w[spinach], - stage: 'test', - commands: "ls\npwd\nspinach" }) + stage: 'test' }) end end end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 2ac7509cb6d..ee3eea9c27a 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -66,131 +66,15 @@ describe Gitlab::Ci::Config::Node::Job do expect(entry.value) .to eq(before_script: %w[ls pwd], script: %w[rspec], - commands: "ls\npwd\nrspec", stage: 'test', after_script: %w[cleanup]) end end end - describe '#before_script' do - context 'when global entry has before script' do - before do - allow(global).to receive(:before_script) - .and_return(%w[ls pwd]) - end - - context 'when before script is overridden' do - let(:config) do - { before_script: %w[whoami], - script: 'rspec' } - end - - it 'returns correct script' do - expect(entry.before_script).to eq %w[whoami] - end - end - - context 'when before script is not overriden' do - let(:config) do - { script: %w[spinach] } - end - - it 'returns correct script' do - expect(entry.before_script).to eq %w[ls pwd] - end - end - end - - context 'when global entry does not have before script' do - before do - allow(global).to receive(:before_script) - .and_return(nil) - end - - context 'when job has before script' do - let(:config) do - { before_script: %w[whoami], - script: 'rspec' } - end - - it 'returns correct script' do - expect(entry.before_script).to eq %w[whoami] - end - end - - context 'when job does not have before script' do - let(:config) do - { script: %w[ls test] } - end - - it 'returns correct script' do - expect(entry.before_script).to be_nil - end - end - end - end - describe '#relevant?' do it 'is a relevant entry' do expect(entry).to be_relevant end end - - describe '#commands' do - context 'when global entry has before script' do - before do - allow(global).to receive(:before_script) - .and_return(%w[ls pwd]) - end - - context 'when before script is overridden' do - let(:config) do - { before_script: %w[whoami], - script: 'rspec' } - end - - it 'returns correct commands' do - expect(entry.commands).to eq "whoami\nrspec" - end - end - - context 'when before script is not overriden' do - let(:config) do - { script: %w[rspec spinach] } - end - - it 'returns correct commands' do - expect(entry.commands).to eq "ls\npwd\nrspec\nspinach" - end - end - end - - context 'when global entry does not have before script' do - before do - allow(global).to receive(:before_script) - .and_return(nil) - end - context 'when job has before script' do - let(:config) do - { before_script: %w[whoami], - script: 'rspec' } - end - - it 'returns correct commands' do - expect(entry.commands).to eq "whoami\nrspec" - end - end - - context 'when job does not have before script' do - let(:config) do - { script: %w[ls test] } - end - - it 'returns correct commands' do - expect(entry.commands).to eq "ls\ntest" - end - end - end - end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index fe7ae61ed81..40837b5f857 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -64,10 +64,8 @@ describe Gitlab::Ci::Config::Node::Jobs do it 'returns key value' do expect(entry.value) .to eq(rspec: { script: %w[rspec], - commands: 'rspec', stage: 'test' }, spinach: { script: %w[spinach], - commands: 'spinach', stage: 'test' }) end end From 3e16b015b969a4d5d28240e76bffd382b0772f49 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 14 Jul 2016 15:23:52 +0200 Subject: [PATCH 046/198] Revert logical validation in CI job stage entry --- lib/ci/gitlab_ci_yaml_processor.rb | 7 ++ lib/gitlab/ci/config/node/job.rb | 1 - lib/gitlab/ci/config/node/stage.rb | 16 ----- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 +- spec/lib/gitlab/ci/config/node/stage_spec.rb | 71 ++------------------ 5 files changed, 15 insertions(+), 84 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 144f9cd7b74..0217a905eac 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -105,6 +105,7 @@ module Ci validate_job_keys!(name, job) validate_job_types!(name, job) + validate_job_stage!(name, job) if job[:stage] validate_job_variables!(name, job) if job[:variables] validate_job_cache!(name, job) if job[:cache] validate_job_artifacts!(name, job) if job[:artifacts] @@ -153,6 +154,12 @@ module Ci end end + def validate_job_stage!(name, job) + unless job[:stage].is_a?(String) && job[:stage].in?(@stages) + raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" + end + end + def validate_job_variables!(name, job) unless validate_variables(job[:variables]) raise ValidationError, diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index f01a46a8ddc..cca9791fc8e 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -12,7 +12,6 @@ module Gitlab validates :config, presence: true with_options on: :processed do - validates :global, required: true validates :name, presence: true validates :name, type: Symbol end diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/node/stage.rb index 909358ea170..cbc97641f5a 100644 --- a/lib/gitlab/ci/config/node/stage.rb +++ b/lib/gitlab/ci/config/node/stage.rb @@ -10,22 +10,6 @@ module Gitlab validations do validates :config, type: String - - with_options on: :processed do - validates :global, required: true - - validate do - unless known? - errors.add(:config, - 'should be one of defined stages ' \ - "(#{global.stages.join(', ')})") - end - end - end - end - - def known? - @global.stages.include?(@config) end def self.default diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index e88f5cfc6dd..daa02faf6fb 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1089,14 +1089,14 @@ EOT config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be one of defined stages (build, test, deploy)") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") end it "returns errors if job stage is not a defined stage" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be one of defined stages (build, test)") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") end it "returns errors if stages is not an array" do diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb index 004012f8b38..fb9ec70762a 100644 --- a/spec/lib/gitlab/ci/config/node/stage_spec.rb +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -1,17 +1,12 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Stage do - let(:stage) { described_class.new(config, global: global) } - let(:global) { spy('Global') } + let(:stage) { described_class.new(config) } describe 'validations' do context 'when stage config value is correct' do let(:config) { 'build' } - before do - allow(global).to receive(:stages).and_return(%w[build]) - end - describe '#value' do it 'returns a stage key' do expect(stage.value).to eq config @@ -25,66 +20,12 @@ describe Gitlab::Ci::Config::Node::Stage do end end - context 'when stage config is incorrect' do - describe '#errors' do - context 'when reference to global node is not set' do - let(:stage) { described_class.new('test') } + context 'when value has a wrong type' do + let(:config) { { test: true } } - it 'raises error' do - expect { stage.validate! }.to raise_error( - Gitlab::Ci::Config::Node::Entry::InvalidError, - /Entry needs global attribute set internally./ - ) - end - end - - context 'when value has a wrong type' do - let(:config) { { test: true } } - - it 'reports errors about wrong type' do - expect(stage.errors) - .to include 'stage config should be a string' - end - end - - context 'when stage is not present in global configuration' do - let(:config) { 'unknown' } - - before do - allow(global) - .to receive(:stages).and_return(%w[test deploy]) - end - - it 'reports error about missing stage' do - stage.validate! - - expect(stage.errors) - .to include 'stage config should be one of ' \ - 'defined stages (test, deploy)' - end - end - end - end - end - - describe '#known?' do - before do - allow(global).to receive(:stages).and_return(%w[test deploy]) - end - - context 'when stage is not known' do - let(:config) { :unknown } - - it 'returns false' do - expect(stage.known?).to be false - end - end - - context 'when stage is known' do - let(:config) { 'test' } - - it 'returns false' do - expect(stage.known?).to be true + it 'reports errors about wrong type' do + expect(stage.errors) + .to include 'stage config should be a string' end end end From 5923741fe690a688591ad36da894b3103954a437 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 14 Jul 2016 15:45:29 +0200 Subject: [PATCH 047/198] Remove references to global entry in new CI config --- lib/gitlab/ci/config/node/entry.rb | 2 +- lib/gitlab/ci/config/node/factory.rb | 1 - spec/lib/gitlab/ci/config/node/factory_spec.rb | 8 +------- spec/lib/gitlab/ci/config/node/job_spec.rb | 6 ++---- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 3 +-- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index c95e85200d2..9640103ea22 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -9,7 +9,7 @@ module Gitlab class InvalidError < StandardError; end attr_reader :config, :attributes - attr_accessor :key, :parent, :global, :description + attr_accessor :key, :parent, :description def initialize(config, **attributes) @config = config diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 339548e0feb..b509c5edf59 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -63,7 +63,6 @@ module Gitlab node.new(value).tap do |entry| entry.key = @attributes[:key] entry.parent = @attributes[:parent] || @parent - entry.global = @attributes[:global] || @parent.global entry.description = @attributes[:description] end end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index bc6bf32ffbf..4e6f2419e13 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -5,11 +5,6 @@ describe Gitlab::Ci::Config::Node::Factory do let(:factory) { described_class.new(node) } let(:node) { Gitlab::Ci::Config::Node::Script } let(:parent) { double('parent') } - let(:global) { double('global') } - - before do - allow(parent).to receive(:global).and_return(global) - end context 'when setting a concrete value' do it 'creates entry with valid value' do @@ -21,13 +16,12 @@ describe Gitlab::Ci::Config::Node::Factory do expect(entry.value).to eq ['ls', 'pwd'] end - it 'sets parent and global attributes' do + it 'sets parent attributes' do entry = factory .value('ls') .parent(parent) .create! - expect(entry.global).to eq global expect(entry.parent).to eq parent end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index ee3eea9c27a..4c7ac9949cc 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -1,9 +1,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Job do - let(:entry) { described_class.new(config, attributes) } - let(:attributes) { { key: :rspec, global: global } } - let(:global) { double('global', stages: %w[test]) } + let(:entry) { described_class.new(config, key: :rspec) } before do entry.process! @@ -21,7 +19,7 @@ describe Gitlab::Ci::Config::Node::Job do end context 'when job name is empty' do - let(:attributes) { { key: '', global: global } } + let(:entry) { described_class.new(config, key: ''.to_sym) } it 'reports error' do expect(entry.errors) diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 40837b5f857..c4c130abb6d 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -1,8 +1,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Jobs do - let(:entry) { described_class.new(config, global: global) } - let(:global) { double('global', before_script: nil, stages: %w[test]) } + let(:entry) { described_class.new(config) } describe 'validations' do before do From 615c9730e7783e82287d2b65f58da6336d3d2410 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 14 Jul 2016 16:01:18 +0200 Subject: [PATCH 048/198] Remove job cache configfrom legacy yaml processor --- lib/ci/gitlab_ci_yaml_processor.rb | 21 -------------------- lib/gitlab/ci/config/node/job.rb | 6 +++++- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 6 +++--- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 0217a905eac..3e4767cc9f6 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -107,7 +107,6 @@ module Ci validate_job_stage!(name, job) if job[:stage] validate_job_variables!(name, job) if job[:variables] - validate_job_cache!(name, job) if job[:cache] validate_job_artifacts!(name, job) if job[:artifacts] validate_job_dependencies!(name, job) if job[:dependencies] end @@ -167,26 +166,6 @@ module Ci end end - def validate_job_cache!(name, job) - job[:cache].keys.each do |key| - unless ALLOWED_CACHE_KEYS.include? key - raise ValidationError, "#{name} job: cache unknown parameter #{key}" - end - end - - if job[:cache][:key] && !validate_string(job[:cache][:key]) - raise ValidationError, "#{name} job: cache:key parameter should be a string" - end - - if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked]) - raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean" - end - - if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths]) - raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings" - end - end - def validate_job_artifacts!(name, job) job[:artifacts].keys.each do |key| unless ALLOWED_ARTIFACTS_KEYS.include? key diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index cca9791fc8e..483be2a21cc 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -32,7 +32,10 @@ module Gitlab node :after_script, Script, description: 'Commands that will be executed when finishing job.' - helpers :before_script, :script, :stage, :type, :after_script + node :cache, Cache, + description: 'Cache definition for this job.' + + helpers :before_script, :script, :stage, :type, :after_script, :cache def name @key @@ -48,6 +51,7 @@ module Gitlab { before_script: before_script_value, script: script_value, stage: stage_value, + cache: cache_value, after_script: after_script_value } end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index daa02faf6fb..c9602bcca22 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1201,21 +1201,21 @@ EOT config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") end it "returns errors if job cache:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") end it "returns errors if job cache:paths is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") end it "returns errors if job dependencies is not an array of strings" do From 41bcbdd8c2412769a376cd37541ad6e65a1af1f2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 15 Jul 2016 21:07:51 +0200 Subject: [PATCH 049/198] Add metadata to new CI config and expose job name --- lib/ci/gitlab_ci_yaml_processor.rb | 4 +- lib/gitlab/ci/config/node/configurable.rb | 3 +- lib/gitlab/ci/config/node/entry.rb | 7 +--- lib/gitlab/ci/config/node/factory.rb | 10 ++--- lib/gitlab/ci/config/node/global.rb | 5 +-- lib/gitlab/ci/config/node/job.rb | 20 +++++----- lib/gitlab/ci/config/node/jobs.rb | 9 +++-- .../lib/gitlab/ci/config/node/factory_spec.rb | 37 +++++++------------ spec/lib/gitlab/ci/config/node/global_spec.rb | 13 ++++--- spec/lib/gitlab/ci/config/node/job_spec.rb | 9 +++-- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 12 +++--- 11 files changed, 60 insertions(+), 69 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 3e4767cc9f6..0704e8f1683 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -82,7 +82,7 @@ module Ci stage: job[:stage], commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"), tag_list: job[:tags] || [], - name: name, + name: job[:name], only: job[:only], except: job[:except], allow_failure: job[:allow_failure] || false, @@ -113,7 +113,7 @@ module Ci def validate_job_keys!(name, job) job.keys.each do |key| - unless ALLOWED_JOB_KEYS.include? key + unless (ALLOWED_JOB_KEYS + %i[name]).include? key raise ValidationError, "#{name} job: unknown parameter #{key}" end end diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index b33d743e2c3..10b2db86d24 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -28,8 +28,7 @@ module Gitlab def create(key, factory) factory .value(@config[key]) - .parent(self) - .with(key: key) + .with(key: key, parent: self) factory.create! end diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 9640103ea22..011c3be849e 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -11,13 +11,10 @@ module Gitlab attr_reader :config, :attributes attr_accessor :key, :parent, :description - def initialize(config, **attributes) + def initialize(config, **metadata) @config = config @entries = {} - - (@attributes = attributes).each do |attribute, value| - public_send("#{attribute}=", value) - end + @metadata = metadata @validator = self.class.validator.new(self) @validator.validate(:new) diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index b509c5edf59..707b052e6a8 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -10,6 +10,7 @@ module Gitlab def initialize(node) @node = node + @metadata = {} @attributes = {} end @@ -18,8 +19,8 @@ module Gitlab self end - def parent(parent) - @parent = parent + def metadata(metadata) + @metadata.merge!(metadata) self end @@ -30,7 +31,6 @@ module Gitlab def create! raise InvalidFactory unless defined?(@value) - raise InvalidFactory unless defined?(@parent) ## # We assume that unspecified entry is undefined. @@ -60,9 +60,9 @@ module Gitlab end def fabricate(node, value = nil) - node.new(value).tap do |entry| + node.new(value, @metadata).tap do |entry| entry.key = @attributes[:key] - entry.parent = @attributes[:parent] || @parent + entry.parent = @attributes[:parent] entry.description = @attributes[:description] end end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index 4a958735599..bedacd904cc 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -54,9 +54,8 @@ module Gitlab def compose_jobs! factory = Node::Factory.new(Node::Jobs) .value(@config.except(*nodes.keys)) - .parent(self) - .with(key: :jobs, global: self) - .with(description: 'Jobs definition for this pipeline') + .with(key: :jobs, parent: self, + description: 'Jobs definition for this pipeline') @entries[:jobs] = factory.create! end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 483be2a21cc..9280412a638 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -10,11 +10,8 @@ module Gitlab validations do validates :config, presence: true - - with_options on: :processed do - validates :name, presence: true - validates :name, type: Symbol - end + validates :name, presence: true + validates :name, type: Symbol end node :before_script, Script, @@ -38,7 +35,7 @@ module Gitlab helpers :before_script, :script, :stage, :type, :after_script, :cache def name - @key + @metadata[:name] end def value @@ -48,11 +45,12 @@ module Gitlab private def to_hash - { before_script: before_script_value, - script: script_value, - stage: stage_value, - cache: cache_value, - after_script: after_script_value } + { name: name, + before_script: before_script, + script: script, + stage: stage, + cache: cache, + after_script: after_script } end def compose! diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 3c1851b9fea..3cabcd6b763 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -31,14 +31,15 @@ module Gitlab private def create(name, config) - Node::Factory.new(node(name)) + Node::Factory.new(job_class(name)) .value(config || {}) - .parent(self) - .with(key: name, description: "#{name} job definition.") + .metadata(name: name) + .with(key: name, parent: self, + description: "#{name} job definition.") .create! end - def node(name) + def job_class(name) if name.to_s.start_with?('.') Node::HiddenJob else diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index 4e6f2419e13..d26185ba585 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -4,32 +4,20 @@ describe Gitlab::Ci::Config::Node::Factory do describe '#create!' do let(:factory) { described_class.new(node) } let(:node) { Gitlab::Ci::Config::Node::Script } - let(:parent) { double('parent') } context 'when setting a concrete value' do it 'creates entry with valid value' do entry = factory .value(['ls', 'pwd']) - .parent(parent) .create! expect(entry.value).to eq ['ls', 'pwd'] end - it 'sets parent attributes' do - entry = factory - .value('ls') - .parent(parent) - .create! - - expect(entry.parent).to eq parent - end - context 'when setting description' do it 'creates entry with description' do entry = factory .value(['ls', 'pwd']) - .parent(parent) .with(description: 'test description') .create! @@ -42,7 +30,6 @@ describe Gitlab::Ci::Config::Node::Factory do it 'creates entry with custom key' do entry = factory .value(['ls', 'pwd']) - .parent(parent) .with(key: 'test key') .create! @@ -56,7 +43,6 @@ describe Gitlab::Ci::Config::Node::Factory do it 'creates entry with valid parent' do entry = factory .value('ls') - .parent(parent) .with(parent: object) .create! @@ -73,23 +59,28 @@ describe Gitlab::Ci::Config::Node::Factory do end end - context 'when not setting parent object' do - it 'raises error' do - expect { factory.value('ls').create! }.to raise_error( - Gitlab::Ci::Config::Node::Factory::InvalidFactory - ) - end - end - context 'when creating entry with nil value' do it 'creates an undefined entry' do entry = factory .value(nil) - .parent(parent) .create! expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined end end + + context 'when passing metadata' do + let(:node) { spy('node') } + + it 'passes metadata as a parameter' do + factory + .value('some value') + .metadata(some: 'hash') + .create! + + expect(node).to have_received(:new) + .with('some value', { some: 'hash' }) + end + end end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index dfcaebe35be..2a071b57c72 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -128,11 +128,14 @@ describe Gitlab::Ci::Config::Node::Global do describe '#jobs' do it 'returns jobs configuration' do - expect(global.jobs) - .to eq(rspec: { script: %w[rspec ls], - stage: 'test' }, - spinach: { script: %w[spinach], - stage: 'test' }) + expect(global.jobs).to eq( + rspec: { name: :rspec, + script: %w[rspec ls], + stage: 'test' }, + spinach: { name: :spinach, + script: %w[spinach], + stage: 'test' } + ) end end end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 4c7ac9949cc..b2559e6e73c 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Job do - let(:entry) { described_class.new(config, key: :rspec) } + let(:entry) { described_class.new(config, name: :rspec) } before do entry.process! @@ -19,7 +19,7 @@ describe Gitlab::Ci::Config::Node::Job do end context 'when job name is empty' do - let(:entry) { described_class.new(config, key: ''.to_sym) } + let(:entry) { described_class.new(config, name: ''.to_sym) } it 'reports error' do expect(entry.errors) @@ -35,7 +35,7 @@ describe Gitlab::Ci::Config::Node::Job do describe '#errors' do it 'reports error about a config type' do expect(entry.errors) - .to include 'rspec config should be a hash' + .to include 'job config should be a hash' end end end @@ -62,7 +62,8 @@ describe Gitlab::Ci::Config::Node::Job do it 'returns correct value' do expect(entry.value) - .to eq(before_script: %w[ls pwd], + .to eq(name: :rspec, + before_script: %w[ls pwd], script: %w[rspec], stage: 'test', after_script: %w[cleanup]) diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index c4c130abb6d..4f08f2f9b69 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -61,11 +61,13 @@ describe Gitlab::Ci::Config::Node::Jobs do describe '#value' do it 'returns key value' do - expect(entry.value) - .to eq(rspec: { script: %w[rspec], - stage: 'test' }, - spinach: { script: %w[spinach], - stage: 'test' }) + expect(entry.value).to eq( + rspec: { name: :rspec, + script: %w[rspec], + stage: 'test' }, + spinach: { name: :spinach, + script: %w[spinach], + stage: 'test' }) end end From 4bb60b0789a31061cbc81af90b7d5dc558f985b3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 15 Jul 2016 21:39:26 +0200 Subject: [PATCH 050/198] Simplify CI config and remove logical validation --- lib/gitlab/ci/config.rb | 1 - lib/gitlab/ci/config/node/entry.rb | 11 ++--------- lib/gitlab/ci/config/node/global.rb | 11 +---------- lib/gitlab/ci/config/node/jobs.rb | 16 +++++++--------- spec/lib/gitlab/ci/config/node/job_spec.rb | 5 +---- spec/lib/gitlab/ci/config/node/jobs_spec.rb | 5 +---- 6 files changed, 12 insertions(+), 37 deletions(-) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 20f5f8e2ff8..ae82c0db3f1 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -15,7 +15,6 @@ module Gitlab @global = Node::Global.new(@config) @global.process! - @global.validate! end def valid? diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 011c3be849e..559688c1bca 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -8,13 +8,13 @@ module Gitlab class Entry class InvalidError < StandardError; end - attr_reader :config, :attributes + attr_reader :config, :metadata attr_accessor :key, :parent, :description def initialize(config, **metadata) @config = config - @entries = {} @metadata = metadata + @entries = {} @validator = self.class.validator.new(self) @validator.validate(:new) @@ -27,13 +27,6 @@ module Gitlab @entries.each_value(&:process!) end - def validate! - return unless valid? - - @validator.validate(:processed) - @entries.each_value(&:validate!) - end - def leaf? nodes.none? end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index bedacd904cc..3b0d0113d61 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -36,19 +36,13 @@ module Gitlab helpers :before_script, :image, :services, :after_script, :variables, :stages, :types, :cache, :jobs - def initialize(*) - super - - @global = self - end - private def compose! super - compose_stages! compose_jobs! + compose_stages! end def compose_jobs! @@ -65,9 +59,6 @@ module Gitlab # Deprecated `:types` key workaround - if types are defined and # stages are not defined we use types definition as stages. # - # Otherwise we use stages in favor of types, and remove types from - # processing. - # if types_defined? && !stages_defined? @entries[:stages] = @entries[:types] end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 3cabcd6b763..77ff3459958 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -11,23 +11,21 @@ module Gitlab validations do validates :config, type: Hash - with_options on: :processed do - validate do - unless has_visible_job? - errors.add(:config, 'should contain at least one visible job') - end + validate do + unless has_visible_job? + errors.add(:config, 'should contain at least one visible job') end end + + def has_visible_job? + config.any? { |key, _| !key.to_s.start_with?('.') } + end end def nodes @config end - def has_visible_job? - @entries.values.any?(&:relevant?) - end - private def create(name, config) diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index b2559e6e73c..2721908c5d7 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -3,10 +3,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Job do let(:entry) { described_class.new(config, name: :rspec) } - before do - entry.process! - entry.validate! - end + before { entry.process! } describe 'validations' do context 'when entry config value is correct' do diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index 4f08f2f9b69..b8d9c70479c 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -4,10 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do - before do - entry.process! - entry.validate! - end + before { entry.process! } context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } From 17084d42aa4f2a9d58d6b6d30656d5b7cfffe007 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 15 Jul 2016 22:35:29 +0200 Subject: [PATCH 051/198] Simplify abstract class for CI config entry nodes --- lib/gitlab/ci/config/node/configurable.rb | 12 ++++--- lib/gitlab/ci/config/node/entry.rb | 17 +--------- lib/gitlab/ci/config/node/global.rb | 2 +- lib/gitlab/ci/config/node/jobs.rb | 31 +++++++++---------- spec/lib/gitlab/ci/config/node/global_spec.rb | 14 ++++++--- 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 10b2db86d24..93a9a253322 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -25,12 +25,14 @@ module Gitlab private - def create(key, factory) - factory - .value(@config[key]) - .with(key: key, parent: self) + def compose! + self.class.nodes.each do |key, factory| + factory + .value(@config[key]) + .with(key: key, parent: self) - factory.create! + @entries[key] = factory.create! + end end class_methods do diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 559688c1bca..813e394e51b 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -28,11 +28,7 @@ module Gitlab end def leaf? - nodes.none? - end - - def nodes - self.class.nodes + @entries.none? end def descendants @@ -74,10 +70,6 @@ module Gitlab def self.default end - def self.nodes - {} - end - def self.validator Validator end @@ -85,13 +77,6 @@ module Gitlab private def compose! - nodes.each do |key, essence| - @entries[key] = create(key, essence) - end - end - - def create(entry, essence) - raise NotImplementedError end end end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index 3b0d0113d61..b545b78a940 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -47,7 +47,7 @@ module Gitlab def compose_jobs! factory = Node::Factory.new(Node::Jobs) - .value(@config.except(*nodes.keys)) + .value(@config.except(*self.class.nodes.keys)) .with(key: :jobs, parent: self, description: 'Jobs definition for this pipeline') diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 77ff3459958..908c8f4f120 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -22,27 +22,24 @@ module Gitlab end end - def nodes - @config - end - private - def create(name, config) - Node::Factory.new(job_class(name)) - .value(config || {}) - .metadata(name: name) - .with(key: name, parent: self, - description: "#{name} job definition.") - .create! + def compose! + @config.each do |name, config| + node = hidden?(name) ? Node::HiddenJob : Node::Job + + factory = Node::Factory.new(node) + .value(config || {}) + .metadata(name: name) + .with(key: name, parent: self, + description: "#{name} job definition.") + + @entries[name] = factory.create! + end end - def job_class(name) - if name.to_s.start_with?('.') - Node::HiddenJob - else - Node::Job - end + def hidden?(name) + name.to_s.start_with?('.') end end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 2a071b57c72..2f87d270b36 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -51,11 +51,11 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.descendants.second.description) .to eq 'Docker image that will be used to execute jobs.' end - end - describe '#leaf?' do - it 'is not leaf' do - expect(global).not_to be_leaf + describe '#leaf?' do + it 'is not leaf' do + expect(global).not_to be_leaf + end end end @@ -65,6 +65,12 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.before_script).to be nil end end + + describe '#leaf?' do + it 'is leaf' do + expect(global).to be_leaf + end + end end context 'when processed' do From 27e88efceb9d59affebf93be040b0a9b0bf31b2f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 18 Jul 2016 09:54:38 +0200 Subject: [PATCH 052/198] Move job image and services nodes to new CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 8 -------- lib/gitlab/ci/config/node/job.rb | 11 ++++++++++- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 8 ++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 0704e8f1683..b8d84de2dbe 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -120,14 +120,6 @@ module Ci end def validate_job_types!(name, job) - if job[:image] && !validate_string(job[:image]) - raise ValidationError, "#{name} job: image should be a string" - end - - if job[:services] && !validate_array_of_strings(job[:services]) - raise ValidationError, "#{name} job: services should be an array of strings" - end - if job[:tags] && !validate_array_of_strings(job[:tags]) raise ValidationError, "#{name} job: tags parameter should be an array of strings" end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 9280412a638..1c28969be14 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -32,7 +32,14 @@ module Gitlab node :cache, Cache, description: 'Cache definition for this job.' - helpers :before_script, :script, :stage, :type, :after_script, :cache + node :image, Image, + description: 'Image that will be used to execute this job.' + + node :services, Services, + description: 'Services that will be used to execute this job.' + + helpers :before_script, :script, :stage, :type, :after_script, + :cache, :image, :services def name @metadata[:name] @@ -48,6 +55,8 @@ module Gitlab { name: name, before_script: before_script, script: script, + image: image, + services: services, stage: stage, cache: cache, after_script: after_script } diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index d629bd252e2..6e6898e758c 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1012,7 +1012,7 @@ EOT config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a string") end it "returns errors if services parameter is not an array" do @@ -1033,14 +1033,14 @@ EOT config = YAML.dump({ rspec: { script: "test", services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") end it "returns errors if job services parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") end it "returns error if job configuration is invalid" do @@ -1054,7 +1054,7 @@ EOT config = YAML.dump({ extra: { services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings") end it "returns errors if there are no jobs defined" do From 1bf9e34713b414f0e1ac8bbfe464a4cd5300581f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 18 Jul 2016 12:37:42 +0200 Subject: [PATCH 053/198] Move except and only job nodes to new CI config --- lib/gitlab/ci/config/node/job.rb | 10 +++- lib/gitlab/ci/config/node/while.rb | 26 +++++++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 +- spec/lib/gitlab/ci/config/node/while_spec.rb | 56 ++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 lib/gitlab/ci/config/node/while.rb create mode 100644 spec/lib/gitlab/ci/config/node/while_spec.rb diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 1c28969be14..401611def17 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -38,8 +38,14 @@ module Gitlab node :services, Services, description: 'Services that will be used to execute this job.' + node :only, While, + description: 'Refs policy this job will be executed for.' + + node :except, While, + description: 'Refs policy this job will be executed for.' + helpers :before_script, :script, :stage, :type, :after_script, - :cache, :image, :services + :cache, :image, :services, :only, :except def name @metadata[:name] @@ -59,6 +65,8 @@ module Gitlab services: services, stage: stage, cache: cache, + only: only, + except: except, after_script: after_script } end diff --git a/lib/gitlab/ci/config/node/while.rb b/lib/gitlab/ci/config/node/while.rb new file mode 100644 index 00000000000..84d4352624d --- /dev/null +++ b/lib/gitlab/ci/config/node/while.rb @@ -0,0 +1,26 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a ref and trigger policy for the job. + # + class While < Entry + include Validatable + + validations do + include LegacyValidationHelpers + + validate :array_of_strings_or_regexps + + def array_of_strings_or_regexps + unless validate_array_of_strings_or_regexps(config) + errors.add(:config, 'should be an array of strings or regexps') + end + end + end + end + end + end + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 6e6898e758c..429ffd6ef35 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -163,7 +163,7 @@ module Ci shared_examples 'raises an error' do it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps') + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:only config should be an array of strings or regexps') end end @@ -319,7 +319,7 @@ module Ci shared_examples 'raises an error' do it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps') + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:except config should be an array of strings or regexps') end end diff --git a/spec/lib/gitlab/ci/config/node/while_spec.rb b/spec/lib/gitlab/ci/config/node/while_spec.rb new file mode 100644 index 00000000000..aac2ed7b3db --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/while_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::While do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is valid' do + context 'when config is a branch or tag name' do + let(:config) { %w[master feature/branch] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq config + end + end + end + + context 'when config is a regexp' do + let(:config) { ['/^issue-.*$/'] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when config is a special keyword' do + let(:config) { %w[tags triggers branches] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + + context 'when entry value is not valid' do + let(:config) { [1] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'while config should be an array of strings or regexps' + end + end + end + end +end From 47fa9f33ca552e085df2158db41b614a79f3651f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 18 Jul 2016 13:09:00 +0200 Subject: [PATCH 054/198] Move job variables config entry to new CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 16 ---------------- lib/gitlab/ci/config/node/job.rb | 6 +++++- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index b8d84de2dbe..7306fd110bd 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -106,7 +106,6 @@ module Ci validate_job_types!(name, job) validate_job_stage!(name, job) if job[:stage] - validate_job_variables!(name, job) if job[:variables] validate_job_artifacts!(name, job) if job[:artifacts] validate_job_dependencies!(name, job) if job[:dependencies] end @@ -124,14 +123,6 @@ module Ci raise ValidationError, "#{name} job: tags parameter should be an array of strings" end - if job[:only] && !validate_array_of_strings_or_regexps(job[:only]) - raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps" - end - - if job[:except] && !validate_array_of_strings_or_regexps(job[:except]) - raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps" - end - if job[:allow_failure] && !validate_boolean(job[:allow_failure]) raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" end @@ -151,13 +142,6 @@ module Ci end end - def validate_job_variables!(name, job) - unless validate_variables(job[:variables]) - raise ValidationError, - "#{name} job: variables should be a map of key-value strings" - end - end - def validate_job_artifacts!(name, job) job[:artifacts].keys.each do |key| unless ALLOWED_ARTIFACTS_KEYS.include? key diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 401611def17..d2113556a08 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -44,8 +44,11 @@ module Gitlab node :except, While, description: 'Refs policy this job will be executed for.' + node :variables, Variables, + description: 'Environment variables available for this job.' + helpers :before_script, :script, :stage, :type, :after_script, - :cache, :image, :services, :only, :except + :cache, :image, :services, :only, :except, :variables def name @metadata[:name] @@ -67,6 +70,7 @@ module Gitlab cache: cache, only: only, except: except, + variables: variables_defined? ? variables : nil, after_script: after_script } end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 429ffd6ef35..fe2c109f463 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -534,7 +534,7 @@ module Ci expect { GitlabCiYamlProcessor.new(config, path) } .to raise_error(GitlabCiYamlProcessor::ValidationError, - /job: variables should be a map/) + /jobs:rspec:variables config should be a hash of key value pairs/) end end From 24b686ebb64e2f5a02d812e9aa726f1ba0868c2e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 18 Jul 2016 14:15:01 +0200 Subject: [PATCH 055/198] Move job artifacts configuration new CI config code --- lib/ci/gitlab_ci_yaml_processor.rb | 20 --------- lib/gitlab/ci/config/node/artifacts.rb | 32 ++++++++++++++ lib/gitlab/ci/config/node/attributable.rb | 23 ++++++++++ lib/gitlab/ci/config/node/job.rb | 7 ++- lib/gitlab/ci/config/node/validators.rb | 10 +++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 12 +++--- .../gitlab/ci/config/node/artifacts_spec.rb | 34 +++++++++++++++ .../ci/config/node/attributable_spec.rb | 43 +++++++++++++++++++ 8 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 lib/gitlab/ci/config/node/artifacts.rb create mode 100644 lib/gitlab/ci/config/node/attributable.rb create mode 100644 spec/lib/gitlab/ci/config/node/artifacts_spec.rb create mode 100644 spec/lib/gitlab/ci/config/node/attributable_spec.rb diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 7306fd110bd..ec85cf1bd3d 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -148,26 +148,6 @@ module Ci raise ValidationError, "#{name} job: artifacts unknown parameter #{key}" end end - - if job[:artifacts][:name] && !validate_string(job[:artifacts][:name]) - raise ValidationError, "#{name} job: artifacts:name parameter should be a string" - end - - if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked]) - raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean" - end - - if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths]) - raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings" - end - - if job[:artifacts][:when] && !job[:artifacts][:when].in?(%w[on_success on_failure always]) - raise ValidationError, "#{name} job: artifacts:when parameter should be on_success, on_failure or always" - end - - if job[:artifacts][:expire_in] && !validate_duration(job[:artifacts][:expire_in]) - raise ValidationError, "#{name} job: artifacts:expire_in parameter should be a duration" - end end def validate_job_dependencies!(name, job) diff --git a/lib/gitlab/ci/config/node/artifacts.rb b/lib/gitlab/ci/config/node/artifacts.rb new file mode 100644 index 00000000000..7b3eb7e1992 --- /dev/null +++ b/lib/gitlab/ci/config/node/artifacts.rb @@ -0,0 +1,32 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a configuration of job artifacts. + # + class Artifacts < Entry + include Validatable + include Attributable + + attributes :name, :untracked, :paths, :when, :expire_in + + validations do + validates :config, type: Hash + + with_options allow_nil: true do + validates :name, type: String + validates :untracked, boolean: true + validates :paths, array_of_strings: true + validates :when, + inclusion: { in: %w[on_success on_failure always], + message: 'should be on_success, on_failure ' \ + 'or always' } + validates :expire_in, duration: true + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/attributable.rb b/lib/gitlab/ci/config/node/attributable.rb new file mode 100644 index 00000000000..6e935c025e4 --- /dev/null +++ b/lib/gitlab/ci/config/node/attributable.rb @@ -0,0 +1,23 @@ +module Gitlab + module Ci + class Config + module Node + module Attributable + extend ActiveSupport::Concern + + class_methods do + def attributes(*attributes) + attributes.each do |attribute| + define_method(attribute) do + return unless config.is_a?(Hash) + + config[attribute] + end + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index d2113556a08..a6d7f16769c 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -47,8 +47,12 @@ module Gitlab node :variables, Variables, description: 'Environment variables available for this job.' + node :artifacts, Artifacts, + description: 'Artifacts configuration for this job.' + helpers :before_script, :script, :stage, :type, :after_script, - :cache, :image, :services, :only, :except, :variables + :cache, :image, :services, :only, :except, :variables, + :artifacts def name @metadata[:name] @@ -71,6 +75,7 @@ module Gitlab only: only, except: except, variables: variables_defined? ? variables : nil, + artifacts: artifacts, after_script: after_script } end diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index d33b407af68..5568be80166 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -33,6 +33,16 @@ module Gitlab end end + class DurationValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_duration(value) + record.errors.add(attribute, 'should be a duration') + end + end + end + class RequiredValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) if value.nil? diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index fe2c109f463..28849bcf3f4 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1138,42 +1138,42 @@ EOT config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") end it "returns errors if job artifacts:when is not an a predefined value" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") end it "returns errors if job artifacts:expire_in is not an a string" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:expire_in is not an a valid duration" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") end it "returns errors if job artifacts:paths is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") end it "returns errors if cache:untracked is not an array of strings" do diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb new file mode 100644 index 00000000000..4973f7d599a --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Artifacts do + let(:entry) { described_class.new(config) } + + describe 'validation' do + context 'when entry config value is correct' do + let(:config) { { paths: %w[public/] } } + + describe '#value' do + it 'returns image string' do + expect(entry.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + let(:config) { { name: 10 } } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'artifacts name should be a string' + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/attributable_spec.rb b/spec/lib/gitlab/ci/config/node/attributable_spec.rb new file mode 100644 index 00000000000..24d9daafd88 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/attributable_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Attributable do + let(:node) { Class.new } + let(:instance) { node.new } + + before do + node.include(described_class) + + node.class_eval do + attributes :name, :test + end + end + + context 'config is a hash' do + before do + allow(instance) + .to receive(:config) + .and_return({ name: 'some name', test: 'some test' }) + end + + it 'returns the value of config' do + expect(instance.name).to eq 'some name' + expect(instance.test).to eq 'some test' + end + + it 'returns no method error for unknown attributes' do + expect { instance.unknown }.to raise_error(NoMethodError) + end + end + + context 'config is not a hash' do + before do + allow(instance) + .to receive(:config) + .and_return('some test') + end + + it 'returns nil' do + expect(instance.test).to be_nil + end + end +end From 7cef4f1908d99743bf1dfb9c1cfdd6b2936b2b3d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 18 Jul 2016 15:38:06 +0200 Subject: [PATCH 056/198] Improve valid keys validation for CI config nodes --- lib/ci/gitlab_ci_yaml_processor.rb | 9 -------- lib/gitlab/ci/config/node/artifacts.rb | 2 ++ lib/gitlab/ci/config/node/cache.rb | 8 +++---- lib/gitlab/ci/config/node/validator.rb | 8 +------ lib/gitlab/ci/config/node/validators.rb | 9 ++++---- .../gitlab/ci/config/node/artifacts_spec.rb | 21 ++++++++++++++----- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index ec85cf1bd3d..6901dfbd15d 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -106,7 +106,6 @@ module Ci validate_job_types!(name, job) validate_job_stage!(name, job) if job[:stage] - validate_job_artifacts!(name, job) if job[:artifacts] validate_job_dependencies!(name, job) if job[:dependencies] end @@ -142,14 +141,6 @@ module Ci end end - def validate_job_artifacts!(name, job) - job[:artifacts].keys.each do |key| - unless ALLOWED_ARTIFACTS_KEYS.include? key - raise ValidationError, "#{name} job: artifacts unknown parameter #{key}" - end - end - end - def validate_job_dependencies!(name, job) unless validate_array_of_strings(job[:dependencies]) raise ValidationError, "#{name} job: dependencies parameter should be an array of strings" diff --git a/lib/gitlab/ci/config/node/artifacts.rb b/lib/gitlab/ci/config/node/artifacts.rb index 7b3eb7e1992..2c301cf2917 100644 --- a/lib/gitlab/ci/config/node/artifacts.rb +++ b/lib/gitlab/ci/config/node/artifacts.rb @@ -13,6 +13,8 @@ module Gitlab validations do validates :config, type: Hash + validates :config, + allowed_keys: %i[name untracked paths when expire_in] with_options allow_nil: true do validates :name, type: String diff --git a/lib/gitlab/ci/config/node/cache.rb b/lib/gitlab/ci/config/node/cache.rb index cdf8ba2e35d..21d96b220b8 100644 --- a/lib/gitlab/ci/config/node/cache.rb +++ b/lib/gitlab/ci/config/node/cache.rb @@ -8,6 +8,10 @@ module Gitlab class Cache < Entry include Configurable + validations do + validates :config, allowed_keys: %i[key untracked paths] + end + node :key, Node::Key, description: 'Cache key used to define a cache affinity.' @@ -16,10 +20,6 @@ module Gitlab node :paths, Node::Paths, description: 'Specify which paths should be cached across builds.' - - validations do - validates :config, allowed_keys: true - end end end end diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb index ca000f245aa..43c7e102b50 100644 --- a/lib/gitlab/ci/config/node/validator.rb +++ b/lib/gitlab/ci/config/node/validator.rb @@ -21,12 +21,6 @@ module Gitlab 'Validator' end - def unknown_keys - return [] unless config.is_a?(Hash) - - config.keys - @node.class.nodes.keys - end - private def location @@ -35,7 +29,7 @@ module Gitlab end def key_name - if key.blank? || key.nil? + if key.blank? @node.class.name.demodulize.underscore.humanize else key diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index 5568be80166..b43a0bc0bab 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -5,10 +5,11 @@ module Gitlab module Validators class AllowedKeysValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - if record.unknown_keys.any? - unknown_list = record.unknown_keys.join(', ') - record.errors.add(:config, - "contains unknown keys: #{unknown_list}") + unknown_keys = record.config.try(:keys).to_a - options[:in] + + if unknown_keys.any? + record.errors.add(:config, 'contains unknown keys: ' + + unknown_keys.join(', ')) end end end diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb index 4973f7d599a..418a88cabac 100644 --- a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb @@ -21,12 +21,23 @@ describe Gitlab::Ci::Config::Node::Artifacts do end context 'when entry value is not correct' do - let(:config) { { name: 10 } } - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'artifacts name should be a string' + context 'when value of attribute is invalid' do + let(:config) { { name: 10 } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts name should be a string' + end + end + + context 'when there is uknown key' do + let(:config) { { test: 100 } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts config contains unknown keys: test' + end end end end From 6d466733a2ab90f2a4d9904e7bfe1ef887568210 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 18 Jul 2016 15:46:24 +0200 Subject: [PATCH 057/198] Validate allowed keys only in new CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 19 ------------------- lib/gitlab/ci/config/node/job.rb | 5 +++++ spec/lib/gitlab/ci/config/node/job_spec.rb | 10 ++++++++++ 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 6901dfbd15d..5c54489ab3f 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -4,15 +4,6 @@ module Ci include Gitlab::Ci::Config::Node::LegacyValidationHelpers - DEFAULT_STAGE = 'test' - ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache] - ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, - :allow_failure, :type, :stage, :when, :artifacts, :cache, - :dependencies, :before_script, :after_script, :variables, - :environment] - ALLOWED_CACHE_KEYS = [:key, :untracked, :paths] - ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in] - attr_reader :path, :cache, :stages def initialize(config, path = nil) @@ -102,21 +93,11 @@ module Ci def validate_job!(name, job) raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) - validate_job_keys!(name, job) validate_job_types!(name, job) - validate_job_stage!(name, job) if job[:stage] validate_job_dependencies!(name, job) if job[:dependencies] end - def validate_job_keys!(name, job) - job.keys.each do |key| - unless (ALLOWED_JOB_KEYS + %i[name]).include? key - raise ValidationError, "#{name} job: unknown parameter #{key}" - end - end - end - def validate_job_types!(name, job) if job[:tags] && !validate_array_of_strings(job[:tags]) raise ValidationError, "#{name} job: tags parameter should be an array of strings" diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index a6d7f16769c..48569a7e890 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -9,6 +9,11 @@ module Gitlab include Configurable validations do + validates :config, allowed_keys: + %i[tags script only except type image services allow_failure + type stage when artifacts cache dependencies before_script + after_script variables environment] + validates :config, presence: true validates :name, presence: true validates :name, type: Symbol diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 2721908c5d7..1484fb60dd8 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -46,6 +46,16 @@ describe Gitlab::Ci::Config::Node::Job do end end end + + context 'when unknown keys detected' do + let(:config) { { unknown: true } } + + describe '#valid' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end end end From 943ae747eac04e19c2614a5b48f41387c6d150d3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 18 Jul 2016 16:33:20 +0200 Subject: [PATCH 058/198] Move tags and allow_failure CI entries to new config --- lib/ci/gitlab_ci_yaml_processor.rb | 8 -------- lib/gitlab/ci/config/node/job.rb | 8 ++++++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 5c54489ab3f..3b6fcd909f4 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -99,14 +99,6 @@ module Ci end def validate_job_types!(name, job) - if job[:tags] && !validate_array_of_strings(job[:tags]) - raise ValidationError, "#{name} job: tags parameter should be an array of strings" - end - - if job[:allow_failure] && !validate_boolean(job[:allow_failure]) - raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" - end - if job[:when] && !job[:when].in?(%w[on_success on_failure always]) raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always" end diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index 48569a7e890..c0ec2236231 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -7,6 +7,9 @@ module Gitlab # class Job < Entry include Configurable + include Attributable + + attributes :tags, :allow_failure validations do validates :config, allowed_keys: @@ -17,6 +20,11 @@ module Gitlab validates :config, presence: true validates :name, presence: true validates :name, type: Symbol + + with_options allow_nil: true do + validates :tags, array_of_strings: true + validates :allow_failure, boolean: true + end end node :before_script, Script, diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 28849bcf3f4..d354ddb7b13 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -956,7 +956,7 @@ EOT config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") end it "returns errors if before_script parameter is invalid" do @@ -1075,7 +1075,7 @@ EOT config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") end it "returns errors if job stage is not a string" do From bb8bf6427d80cb4858318a44e395a2d1cd9115b7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 13:08:28 +0200 Subject: [PATCH 059/198] Move job environment validation to new CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 11 ----------- lib/gitlab/ci/config/node/job.rb | 14 +++++++++++++- .../ci/config/node/legacy_validation_helpers.rb | 4 ---- lib/gitlab/ci/config/node/validators.rb | 3 ++- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 6 +++--- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 3b6fcd909f4..aa2f7743a5e 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -93,21 +93,10 @@ module Ci def validate_job!(name, job) raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) - validate_job_types!(name, job) validate_job_stage!(name, job) if job[:stage] validate_job_dependencies!(name, job) if job[:dependencies] end - def validate_job_types!(name, job) - if job[:when] && !job[:when].in?(%w[on_success on_failure always]) - raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always" - end - - if job[:environment] && !validate_environment(job[:environment]) - raise ValidationError, "#{name} job: environment parameter #{Gitlab::Regex.environment_name_regex_message}" - end - end - def validate_job_stage!(name, job) unless job[:stage].is_a?(String) && job[:stage].in?(@stages) raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index c0ec2236231..464abd4d2d8 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -9,7 +9,7 @@ module Gitlab include Configurable include Attributable - attributes :tags, :allow_failure + attributes :tags, :allow_failure, :when, :environment validations do validates :config, allowed_keys: @@ -24,6 +24,18 @@ module Gitlab with_options allow_nil: true do validates :tags, array_of_strings: true validates :allow_failure, boolean: true + validates :when, + inclusion: { in: %w[on_success on_failure always], + message: 'should be on_success, on_failure ' \ + 'or always' } + validates :environment, + type: { + with: String, + message: Gitlab::Regex.environment_name_regex_message } + validates :environment, + format: { + with: Gitlab::Regex.environment_name_regex, + message: Gitlab::Regex.environment_name_regex_message } end end diff --git a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb index 4d9a508796a..0c291efe6a5 100644 --- a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb @@ -41,10 +41,6 @@ module Gitlab false end - def validate_environment(value) - value.is_a?(String) && value =~ Gitlab::Regex.environment_name_regex - end - def validate_boolean(value) value.in?([true, false]) end diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index b43a0bc0bab..23d5faf6f07 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -69,7 +69,8 @@ module Gitlab raise unless type.is_a?(Class) unless value.is_a?(type) - record.errors.add(attribute, "should be a #{type.name}") + message = options[:message] || "should be a #{type.name}" + record.errors.add(attribute, message) end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index d354ddb7b13..9735af27aa1 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -754,7 +754,7 @@ module Ci let(:environment) { 1 } it 'raises error' do - expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}") + expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") end end @@ -762,7 +762,7 @@ module Ci let(:environment) { 'production staging' } it 'raises error' do - expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}") + expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") end end end @@ -1131,7 +1131,7 @@ EOT config = YAML.dump({ rspec: { script: "test", when: 1 } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure or always") end it "returns errors if job artifacts:name is not an a string" do From c0ebfea6baf330ed5ae5bc870407fe47f76119cf Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 30 Jun 2016 14:41:19 +0200 Subject: [PATCH 060/198] Fix some useless access modifiers in the code --- .../import/bitbucket_controller.rb | 2 -- app/controllers/import/gitlab_controller.rb | 2 -- app/helpers/diff_helper.rb | 2 -- app/models/concerns/token_authenticatable.rb | 28 +++++++++---------- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 25e58724860..944c73d139a 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -82,8 +82,6 @@ class Import::BitbucketController < Import::BaseController go_to_bitbucket_for_permissions end - private - def access_params { bitbucket_access_token: session[:bitbucket_access_token], diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 23a396e8084..08130ee8176 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -61,8 +61,6 @@ class Import::GitlabController < Import::BaseController go_to_gitlab_for_permissions end - private - def access_params { gitlab_access_token: session[:gitlab_access_token] } end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 75b029365f9..05366d06b1c 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -142,8 +142,6 @@ module DiffHelper toggle_whitespace_link(url, options) end - private - def hide_whitespace? params[:w] == '1' end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 885deaf78d2..27a03b3bada 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -1,6 +1,20 @@ module TokenAuthenticatable extend ActiveSupport::Concern + private + + def write_new_token(token_field) + new_token = generate_token(token_field) + write_attribute(token_field, new_token) + end + + def generate_token(token_field) + loop do + token = Devise.friendly_token + break token unless self.class.unscoped.find_by(token_field => token) + end + end + class_methods do def authentication_token_fields @token_fields || [] @@ -32,18 +46,4 @@ module TokenAuthenticatable end end end - - private - - def write_new_token(token_field) - new_token = generate_token(token_field) - write_attribute(token_field, new_token) - end - - def generate_token(token_field) - loop do - token = Devise.friendly_token - break token unless self.class.unscoped.find_by(token_field => token) - end - end end From 1cf164f14a09fcb6322c33f62e80496568f83d8e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 14:17:47 +0200 Subject: [PATCH 061/198] Fix private method visibility in container registry --- .../container_registry_authentication_service.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index e294a962352..6072123b851 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -24,10 +24,14 @@ module Auth token[:access] = names.map do |name| { type: 'repository', name: name, actions: %w(*) } end - + token.encoded end + def self.token_expire_at + Time.now + current_application_settings.container_registry_token_expire_delay.minutes + end + private def authorized_token(*accesses) @@ -35,7 +39,7 @@ module Auth token.issuer = registry.issuer token.audience = params[:service] token.subject = current_user.try(:username) - token.expire_time = ContainerRegistryAuthenticationService.token_expire_at + token.expire_time = self.class.token_expire_at token[:access] = accesses.compact token end @@ -81,9 +85,5 @@ module Auth def registry Gitlab.config.registry end - - def self.token_expire_at - Time.now + current_application_settings.container_registry_token_expire_delay.minutes - end end end From e9c09c92a7b56c40a06600a8856b4d4eaf15008e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 14:18:51 +0200 Subject: [PATCH 062/198] Refactor system notes service to make it singleton --- app/services/system_note_service.rb | 154 ++++++++++++++-------------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1ab3b5789bc..e13dc9265b8 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -2,7 +2,9 @@ # # Used for creating system notes (e.g., when a user references a merge request # from an issue, an issue's assignee changes, an issue is closed, etc.) -class SystemNoteService +module SystemNoteService + extend self + # Called when commits are added to a Merge Request # # noteable - Noteable object @@ -15,7 +17,7 @@ class SystemNoteService # See new_commit_summary and existing_commit_summary. # # Returns the created Note object - def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil) + def add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil) total_count = new_commits.length + existing_commits.length commits_text = "#{total_count} commit".pluralize(total_count) @@ -40,7 +42,7 @@ class SystemNoteService # "Reassigned to @rspeicher" # # Returns the created Note object - def self.change_assignee(noteable, project, author, assignee) + def change_assignee(noteable, project, author, assignee) body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}" create_note(noteable: noteable, project: project, author: author, note: body) @@ -63,7 +65,7 @@ class SystemNoteService # "Removed ~5 label" # # Returns the created Note object - def self.change_label(noteable, project, author, added_labels, removed_labels) + def change_label(noteable, project, author, added_labels, removed_labels) labels_count = added_labels.count + removed_labels.count references = ->(label) { label.to_reference(format: :id) } @@ -101,7 +103,7 @@ class SystemNoteService # "Miletone changed to 7.11" # # Returns the created Note object - def self.change_milestone(noteable, project, author, milestone) + def change_milestone(noteable, project, author, milestone) body = 'Milestone ' body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}" @@ -123,7 +125,7 @@ class SystemNoteService # "Status changed to closed by bc17db76" # # Returns the created Note object - def self.change_status(noteable, project, author, status, source) + def change_status(noteable, project, author, status, source) body = "Status changed to #{status}" body << " by #{source.gfm_reference(project)}" if source @@ -131,26 +133,26 @@ class SystemNoteService end # Called when 'merge when build succeeds' is executed - def self.merge_when_build_succeeds(noteable, project, author, last_commit) + def merge_when_build_succeeds(noteable, project, author, last_commit) body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when 'merge when build succeeds' is canceled - def self.cancel_merge_when_build_succeeds(noteable, project, author) + def cancel_merge_when_build_succeeds(noteable, project, author) body = 'Canceled the automatic merge' create_note(noteable: noteable, project: project, author: author, note: body) end - def self.remove_merge_request_wip(noteable, project, author) + def remove_merge_request_wip(noteable, project, author) body = 'Unmarked this merge request as a Work In Progress' create_note(noteable: noteable, project: project, author: author, note: body) end - def self.add_merge_request_wip(noteable, project, author) + def add_merge_request_wip(noteable, project, author) body = 'Marked this merge request as a **Work In Progress**' create_note(noteable: noteable, project: project, author: author, note: body) @@ -168,7 +170,7 @@ class SystemNoteService # "Title changed from **Old** to **New**" # # Returns the created Note object - def self.change_title(noteable, project, author, old_title) + def change_title(noteable, project, author, old_title) new_title = noteable.title.dup old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs @@ -191,7 +193,7 @@ class SystemNoteService # "Made the issue confidential" # # Returns the created Note object - def self.change_issue_confidentiality(issue, project, author) + def change_issue_confidentiality(issue, project, author) body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible' create_note(noteable: issue, project: project, author: author, note: body) end @@ -210,7 +212,7 @@ class SystemNoteService # "Target branch changed from `Old` to `New`" # # Returns the created Note object - def self.change_branch(noteable, project, author, branch_type, old_branch, new_branch) + def change_branch(noteable, project, author, branch_type, old_branch, new_branch) body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize create_note(noteable: noteable, project: project, author: author, note: body) end @@ -229,7 +231,7 @@ class SystemNoteService # "Restored target branch `feature`" # # Returns the created Note object - def self.change_branch_presence(noteable, project, author, branch_type, branch, presence) + def change_branch_presence(noteable, project, author, branch_type, branch, presence) verb = if presence == :add 'restored' @@ -245,7 +247,7 @@ class SystemNoteService # Example note text: # # "Started branch `201-issue-branch-button`" - def self.new_issue_branch(issue, project, author, branch) + def new_issue_branch(issue, project, author, branch) h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) @@ -270,7 +272,7 @@ class SystemNoteService # See cross_reference_note_content. # # Returns the created Note object - def self.cross_reference(noteable, mentioner, author) + def cross_reference(noteable, mentioner, author) return if cross_reference_disallowed?(noteable, mentioner) gfm_reference = mentioner.gfm_reference(noteable.project) @@ -294,7 +296,7 @@ class SystemNoteService end end - def self.cross_reference?(note_text) + def cross_reference?(note_text) note_text.start_with?(cross_reference_note_prefix) end @@ -308,7 +310,7 @@ class SystemNoteService # mentioner - Mentionable object # # Returns Boolean - def self.cross_reference_disallowed?(noteable, mentioner) + def cross_reference_disallowed?(noteable, mentioner) return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? return false unless mentioner.is_a?(MergeRequest) return false unless noteable.is_a?(Commit) @@ -328,7 +330,7 @@ class SystemNoteService # # Returns Boolean - def self.cross_reference_exists?(noteable, mentioner) + def cross_reference_exists?(noteable, mentioner) # Initial scope should be system notes of this noteable type notes = Note.system.where(noteable_type: noteable.class) @@ -342,9 +344,60 @@ class SystemNoteService notes_for_mentioner(mentioner, noteable, notes).count > 0 end + # Build an Array of lines detailing each commit added in a merge request + # + # new_commits - Array of new Commit objects + # + # Returns an Array of Strings + def new_commit_summary(new_commits) + new_commits.collect do |commit| + "* #{commit.short_id} - #{escape_html(commit.title)}" + end + end + + # Called when the status of a Task has changed + # + # noteable - Noteable object. + # project - Project owning noteable + # author - User performing the change + # new_task - TaskList::Item object. + # + # Example Note text: + # + # "Soandso marked the task Whatever as completed." + # + # Returns the created Note object + def change_task_status(noteable, project, author, new_task) + status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE + body = "Marked the task **#{new_task.source}** as #{status_label}" + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when noteable has been moved to another project + # + # direction - symbol, :to or :from + # noteable - Noteable object + # noteable_ref - Referenced noteable + # author - User performing the move + # + # Example Note text: + # + # "Moved to some_namespace/project_new#11" + # + # Returns the created Note object + def noteable_moved(noteable, project, noteable_ref, author, direction:) + unless [:to, :from].include?(direction) + raise ArgumentError, "Invalid direction `#{direction}`" + end + + cross_reference = noteable_ref.to_reference(project) + body = "Moved #{direction} #{cross_reference}" + create_note(noteable: noteable, project: project, author: author, note: body) + end + private - def self.notes_for_mentioner(mentioner, noteable, notes) + def notes_for_mentioner(mentioner, noteable, notes) if mentioner.is_a?(Commit) notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}") else @@ -353,29 +406,18 @@ class SystemNoteService end end - def self.create_note(args = {}) + def create_note(args = {}) Note.create(args.merge(system: true)) end - def self.cross_reference_note_prefix + def cross_reference_note_prefix 'mentioned in ' end - def self.cross_reference_note_content(gfm_reference) + def cross_reference_note_content(gfm_reference) "#{cross_reference_note_prefix}#{gfm_reference}" end - # Build an Array of lines detailing each commit added in a merge request - # - # new_commits - Array of new Commit objects - # - # Returns an Array of Strings - def self.new_commit_summary(new_commits) - new_commits.collect do |commit| - "* #{commit.short_id} - #{escape_html(commit.title)}" - end - end - # Build a single line summarizing existing commits being added in a merge # request # @@ -392,7 +434,7 @@ class SystemNoteService # "* ea0f8418 - 1 commit from branch `feature`" # # Returns a newline-terminated String - def self.existing_commit_summary(noteable, existing_commits, oldrev = nil) + def existing_commit_summary(noteable, existing_commits, oldrev = nil) return '' if existing_commits.empty? count = existing_commits.size @@ -415,47 +457,7 @@ class SystemNoteService "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" end - # Called when the status of a Task has changed - # - # noteable - Noteable object. - # project - Project owning noteable - # author - User performing the change - # new_task - TaskList::Item object. - # - # Example Note text: - # - # "Soandso marked the task Whatever as completed." - # - # Returns the created Note object - def self.change_task_status(noteable, project, author, new_task) - status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE - body = "Marked the task **#{new_task.source}** as #{status_label}" - create_note(noteable: noteable, project: project, author: author, note: body) - end - - # Called when noteable has been moved to another project - # - # direction - symbol, :to or :from - # noteable - Noteable object - # noteable_ref - Referenced noteable - # author - User performing the move - # - # Example Note text: - # - # "Moved to some_namespace/project_new#11" - # - # Returns the created Note object - def self.noteable_moved(noteable, project, noteable_ref, author, direction:) - unless [:to, :from].include?(direction) - raise ArgumentError, "Invalid direction `#{direction}`" - end - - cross_reference = noteable_ref.to_reference(project) - body = "Moved #{direction} #{cross_reference}" - create_note(noteable: noteable, project: project, author: author, note: body) - end - - def self.escape_html(text) + def escape_html(text) Rack::Utils.escape_html(text) end end From 239d8ab30c660167bd42ff745df3568b16ef82b3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 14:19:54 +0200 Subject: [PATCH 063/198] Refactor gitlab themes module to make it singleton --- lib/gitlab/themes.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb index 83f91de810c..d4020af76f9 100644 --- a/lib/gitlab/themes.rb +++ b/lib/gitlab/themes.rb @@ -2,6 +2,8 @@ module Gitlab # Module containing GitLab's application theme definitions and helper methods # for accessing them. module Themes + extend self + # Theme ID used when no `default_theme` configuration setting is provided. APPLICATION_DEFAULT = 2 @@ -22,7 +24,7 @@ module Gitlab # classes that might be applied to the `body` element # # Returns a String - def self.body_classes + def body_classes THEMES.collect(&:css_class).uniq.join(' ') end @@ -33,26 +35,26 @@ module Gitlab # id - Integer ID # # Returns a Theme - def self.by_id(id) + def by_id(id) THEMES.detect { |t| t.id == id } || default end # Returns the number of defined Themes - def self.count + def count THEMES.size end # Get the default Theme # # Returns a Theme - def self.default + def default by_id(default_id) end # Iterate through each Theme # # Yields the Theme object - def self.each(&block) + def each(&block) THEMES.each(&block) end @@ -61,7 +63,7 @@ module Gitlab # user - User record # # Returns a Theme - def self.for_user(user) + def for_user(user) if user by_id(user.theme_id) else @@ -71,7 +73,7 @@ module Gitlab private - def self.default_id + def default_id id = Gitlab.config.gitlab.default_theme.to_i # Prevent an invalid configuration setting from causing an infinite loop From e15b63b34eba510ba14258f1719ecec2f90b1b49 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 14:21:39 +0200 Subject: [PATCH 064/198] Fix methods visibility in gitlab database module --- lib/gitlab/database.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 078609c86f1..55b8f888d53 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -55,12 +55,12 @@ module Gitlab end end - private - def self.connection ActiveRecord::Base.connection end + private_class_method :connection + def self.database_version row = connection.execute("SELECT VERSION()").first @@ -70,5 +70,7 @@ module Gitlab row.first end end + + private_class_method :database_version end end From 20a5033d79ba8375a79d7d5d8781b53be891ba42 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 14:43:11 +0200 Subject: [PATCH 065/198] Fix method visibility in inline diff class --- lib/gitlab/diff/inline_diff.rb | 74 ++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index 28ad637fda4..55708d42161 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -19,24 +19,6 @@ module Gitlab attr_accessor :old_line, :new_line, :offset - def self.for_lines(lines) - changed_line_pairs = self.find_changed_line_pairs(lines) - - inline_diffs = [] - - changed_line_pairs.each do |old_index, new_index| - old_line = lines[old_index] - new_line = lines[new_index] - - old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs - - inline_diffs[old_index] = old_diffs - inline_diffs[new_index] = new_diffs - end - - inline_diffs - end - def initialize(old_line, new_line, offset: 0) @old_line = old_line[offset..-1] @new_line = new_line[offset..-1] @@ -63,32 +45,54 @@ module Gitlab [old_diffs, new_diffs] end - private + class << self + def for_lines(lines) + changed_line_pairs = find_changed_line_pairs(lines) - # Finds pairs of old/new line pairs that represent the same line that changed - def self.find_changed_line_pairs(lines) - # Prefixes of all diff lines, indicating their types - # For example: `" - + -+ ---+++ --+ -++"` - line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ') + inline_diffs = [] - changed_line_pairs = [] - line_prefixes.scan(LINE_PAIRS_PATTERN) do - # For `"---+++"`, `begin_index == 0`, `end_index == 6` - begin_index, end_index = Regexp.last_match.offset(:del_ins) + changed_line_pairs.each do |old_index, new_index| + old_line = lines[old_index] + new_line = lines[new_index] - # For `"---+++"`, `changed_line_count == 3` - changed_line_count = (end_index - begin_index) / 2 + old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs - halfway_index = begin_index + changed_line_count - (begin_index...halfway_index).each do |i| - # For `"---+++"`, index 1 maps to 1 + 3 = 4 - changed_line_pairs << [i, i + changed_line_count] + inline_diffs[old_index] = old_diffs + inline_diffs[new_index] = new_diffs end + + inline_diffs end - changed_line_pairs + private + + # Finds pairs of old/new line pairs that represent the same line that changed + def find_changed_line_pairs(lines) + # Prefixes of all diff lines, indicating their types + # For example: `" - + -+ ---+++ --+ -++"` + line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ') + + changed_line_pairs = [] + line_prefixes.scan(LINE_PAIRS_PATTERN) do + # For `"---+++"`, `begin_index == 0`, `end_index == 6` + begin_index, end_index = Regexp.last_match.offset(:del_ins) + + # For `"---+++"`, `changed_line_count == 3` + changed_line_count = (end_index - begin_index) / 2 + + halfway_index = begin_index + changed_line_count + (begin_index...halfway_index).each do |i| + # For `"---+++"`, index 1 maps to 1 + 3 = 4 + changed_line_pairs << [i, i + changed_line_count] + end + end + + changed_line_pairs + end end + private + def longest_common_prefix(a, b) max_length = [a.length, b.length].max From f281041fea2884ea666d7de701c8e824078b86cc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 15:05:22 +0200 Subject: [PATCH 066/198] Make banzai module that handles markdown singleton --- lib/banzai/renderer.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 910687a7b6a..a4ae27eefd8 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -1,5 +1,7 @@ module Banzai module Renderer + extend self + # Convert a Markdown String into an HTML-safe String of HTML # # Note that while the returned HTML will have been sanitized of dangerous @@ -14,7 +16,7 @@ module Banzai # context - Hash of context options passed to our HTML Pipeline # # Returns an HTML-safe String - def self.render(text, context = {}) + def render(text, context = {}) cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) @@ -52,7 +54,7 @@ module Banzai # texts_and_contexts # => [{ text: '### Hello', # context: { cache_key: [note, :note] } }] - def self.cache_collection_render(texts_and_contexts) + def cache_collection_render(texts_and_contexts) items_collection = texts_and_contexts.each_with_index do |item, index| context = item[:context] cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline]) @@ -81,7 +83,7 @@ module Banzai items_collection.map { |item| item[:rendered] } end - def self.render_result(text, context = {}) + def render_result(text, context = {}) text = Pipeline[:pre_process].to_html(text, context) if text Pipeline[context[:pipeline]].call(text, context) @@ -100,7 +102,7 @@ module Banzai # :user - User object # # Returns an HTML-safe String - def self.post_process(html, context) + def post_process(html, context) context = Pipeline[context[:pipeline]].transform_context(context) pipeline = Pipeline[:post_process] @@ -113,7 +115,7 @@ module Banzai private - def self.cacheless_render(text, context = {}) + def cacheless_render(text, context = {}) Gitlab::Metrics.measure(:banzai_cacheless_render) do result = render_result(text, context) @@ -126,7 +128,7 @@ module Banzai end end - def self.full_cache_key(cache_key, pipeline_name) + def full_cache_key(cache_key, pipeline_name) return unless cache_key ["banzai", *cache_key, pipeline_name || :full] end @@ -134,7 +136,7 @@ module Banzai # To map Rails.cache.read_multi results we need to know the Rails.cache.expanded_key. # Other option will be to generate stringified keys on our side and don't delegate to Rails.cache.expanded_key # method. - def self.full_cache_multi_key(cache_key, pipeline_name) + def full_cache_multi_key(cache_key, pipeline_name) return unless cache_key Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name)) end From 4d9b34bb7e8eddef4db3e664356b19e121ea381c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 15:05:46 +0200 Subject: [PATCH 067/198] Fix method visibility in gitlab metrics class --- lib/gitlab/metrics.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 49f702f91f6..f2758b5c44a 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -136,10 +136,10 @@ module Gitlab end end - private - def self.current_transaction Transaction.current end + + private_class_method :current_transaction end end From 3d2b6a3d7b51b8caf5a9133b6c0f0a6da3ab6d9d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 15:06:16 +0200 Subject: [PATCH 068/198] Fix methods visibility in markdown filter class --- lib/banzai/filter/markdown_filter.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index 9b209533a89..ff580ec68f8 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -12,7 +12,12 @@ module Banzai html end - private + def self.renderer + @renderer ||= begin + renderer = Redcarpet::Render::HTML.new + Redcarpet::Markdown.new(renderer, redcarpet_options) + end + end def self.redcarpet_options # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use @@ -28,12 +33,7 @@ module Banzai }.freeze end - def self.renderer - @renderer ||= begin - renderer = Redcarpet::Render::HTML.new - Redcarpet::Markdown.new(renderer, redcarpet_options) - end - end + private_class_method :redcarpet_options end end end From b6d3eadfe23997770ca260460c0e22fed6859d45 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 15:06:39 +0200 Subject: [PATCH 069/198] Fix method visiblity in emoji filter class --- lib/banzai/filter/emoji_filter.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index ae7d31cf191..2492b5213ac 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -38,6 +38,11 @@ module Banzai end end + # Build a regexp that matches all valid :emoji: names. + def self.emoji_pattern + @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ + end + private def emoji_url(name) @@ -59,11 +64,6 @@ module Banzai ActionController::Base.helpers.url_to_image(image) end - # Build a regexp that matches all valid :emoji: names. - def self.emoji_pattern - @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ - end - def emoji_pattern self.class.emoji_pattern end From d6f669774481b160c2d963b56309ab6262216c42 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 19 Jul 2016 15:07:52 +0200 Subject: [PATCH 070/198] Enable Rubocop cops for invalid access modifiers This enables following cops: Check for useless access modifiers Lint/UselessAccessModifier Checks for attempts to use `private` or `protected` to set the visibility of a class method, which does not work. Lint/IneffectiveAccessModifier This also disables two false possitives in concerns. --- .rubocop.yml | 9 +++++++++ .rubocop_todo.yml | 8 -------- app/models/concerns/token_authenticatable.rb | 2 +- lib/gitlab/ci/config/node/configurable.rb | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index db0bcfadcf4..3f3a0561710 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -506,6 +506,15 @@ Metrics/PerceivedComplexity: #################### Lint ################################ +# Checks for useless access modifiers. +Lint/UselessAccessModifier: + Enabled: true + +# Checks for attempts to use `private` or `protected` to set the visibility +# of a class method, which does not work. +Lint/IneffectiveAccessModifier: + Enabled: false + # Checks for ambiguous operators in the first argument of a method invocation # without parentheses. Lint/AmbiguousOperator: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9310e711889..05a5fae8543 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -19,10 +19,6 @@ Lint/AssignmentInCondition: Lint/HandleExceptions: Enabled: false -# Offense count: 21 -Lint/IneffectiveAccessModifier: - Enabled: false - # Offense count: 2 Lint/Loop: Enabled: false @@ -48,10 +44,6 @@ Lint/UnusedBlockArgument: Lint/UnusedMethodArgument: Enabled: false -# Offense count: 11 -Lint/UselessAccessModifier: - Enabled: false - # Offense count: 12 # Cop supports --auto-correct. Performance/PushSplat: diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 27a03b3bada..24c7b26d223 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -20,7 +20,7 @@ module TokenAuthenticatable @token_fields || [] end - private + private # rubocop:disable Lint/UselessAccessModifier def add_authentication_token_field(token_field) @token_fields = [] unless @token_fields diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 37936fc8242..2592e1ec244 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -36,7 +36,7 @@ module Gitlab Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] end - private + private # rubocop:disable Lint/UselessAccessModifier def node(symbol, entry_class, metadata) factory = Node::Factory.new(entry_class) From f83bccfe4f98281ed80c95189c5f7ed77799b2b3 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 20 Jul 2016 10:59:49 +0200 Subject: [PATCH 071/198] Add minor readability, style improvements in CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 5 ++--- lib/gitlab/ci/config/node/artifacts.rb | 7 ++++--- lib/gitlab/ci/config/node/attributable.rb | 2 +- lib/gitlab/ci/config/node/entry.rb | 4 ++-- lib/gitlab/ci/config/node/job.rb | 9 +++++---- lib/gitlab/ci/config/node/jobs.rb | 10 +++++----- lib/gitlab/ci/config/node/undefined.rb | 2 +- spec/lib/gitlab/ci/config/node/artifacts_spec.rb | 2 +- 8 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 013813ef00b..24601fdfe85 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -8,7 +8,8 @@ module Ci def initialize(config, path = nil) @ci_config = Gitlab::Ci::Config.new(config) - @config, @path = @ci_config.to_hash, path + @config = @ci_config.to_hash + @path = path unless @ci_config.valid? raise ValidationError, @ci_config.errors.first @@ -120,8 +121,6 @@ module Ci end def validate_job!(name, job) - raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script) - validate_job_stage!(name, job) if job[:stage] validate_job_dependencies!(name, job) if job[:dependencies] end diff --git a/lib/gitlab/ci/config/node/artifacts.rb b/lib/gitlab/ci/config/node/artifacts.rb index 2c301cf2917..844bd2fe998 100644 --- a/lib/gitlab/ci/config/node/artifacts.rb +++ b/lib/gitlab/ci/config/node/artifacts.rb @@ -9,12 +9,13 @@ module Gitlab include Validatable include Attributable - attributes :name, :untracked, :paths, :when, :expire_in + ALLOWED_KEYS = %i[name untracked paths when expire_in] + + attributes ALLOWED_KEYS validations do validates :config, type: Hash - validates :config, - allowed_keys: %i[name untracked paths when expire_in] + validates :config, allowed_keys: ALLOWED_KEYS with_options allow_nil: true do validates :name, type: String diff --git a/lib/gitlab/ci/config/node/attributable.rb b/lib/gitlab/ci/config/node/attributable.rb index 6e935c025e4..221b666f9f6 100644 --- a/lib/gitlab/ci/config/node/attributable.rb +++ b/lib/gitlab/ci/config/node/attributable.rb @@ -7,7 +7,7 @@ module Gitlab class_methods do def attributes(*attributes) - attributes.each do |attribute| + attributes.flatten.each do |attribute| define_method(attribute) do return unless config.is_a?(Hash) diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 813e394e51b..0c782c422b5 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -24,7 +24,7 @@ module Gitlab return unless valid? compose! - @entries.each_value(&:process!) + descendants.each(&:process!) end def leaf? @@ -44,7 +44,7 @@ module Gitlab end def errors - @validator.messages + @entries.values.flat_map(&:errors) + @validator.messages + descendants.flat_map(&:errors) end def value diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index aea9fef8229..dc0813a8c18 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -9,13 +9,14 @@ module Gitlab include Configurable include Attributable + ALLOWED_KEYS = %i[tags script only except type image services allow_failure + type stage when artifacts cache dependencies before_script + after_script variables environment] + attributes :tags, :allow_failure, :when, :environment validations do - validates :config, allowed_keys: - %i[tags script only except type image services allow_failure - type stage when artifacts cache dependencies before_script - after_script variables environment] + validates :config, allowed_keys: ALLOWED_KEYS validates :config, presence: true validates :name, presence: true diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 908c8f4f120..51683c82ceb 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -18,10 +18,14 @@ module Gitlab end def has_visible_job? - config.any? { |key, _| !key.to_s.start_with?('.') } + config.any? { |name, _| !hidden?(name) } end end + def hidden?(name) + name.to_s.start_with?('.') + end + private def compose! @@ -37,10 +41,6 @@ module Gitlab @entries[name] = factory.create! end end - - def hidden?(name) - name.to_s.start_with?('.') - end end end end diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index 384774c9b69..84dab61e7e9 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -5,7 +5,7 @@ module Gitlab ## # This class represents an undefined and unspecified entry node. # - # It decorates original entry adding method that idicates it is + # It decorates original entry adding method that indicates it is # unspecified. # class Undefined < SimpleDelegator diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb index 418a88cabac..beed29b18ae 100644 --- a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb @@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::Node::Artifacts do end end - context 'when there is uknown key' do + context 'when there is an unknown key present' do let(:config) { { test: 100 } } it 'reports error' do From dff10976da42cc729069cddf0f032347fe2f6b14 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 20 Jul 2016 14:15:18 +0200 Subject: [PATCH 072/198] Move job dependencies entry to the new CI config --- lib/ci/gitlab_ci_yaml_processor.rb | 24 ++++++++------------ lib/gitlab/ci/config/node/job.rb | 4 +++- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 24601fdfe85..a2e8bd22a52 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -98,21 +98,22 @@ module Ci @jobs = @ci_config.jobs @jobs.each do |name, job| - validate_job!(name, job) + # logical validation for job + + validate_job_stage!(name, job) + validate_job_dependencies!(name, job) end end def yaml_variables(name) - variables = global_variables.merge(job_variables(name)) + variables = (@variables || {}) + .merge(job_variables(name)) + variables.map do |key, value| { key: key, value: value, public: true } end end - def global_variables - @variables || {} - end - def job_variables(name) job = @jobs[name.to_sym] return {} unless job @@ -120,21 +121,16 @@ module Ci job[:variables] || {} end - def validate_job!(name, job) - validate_job_stage!(name, job) if job[:stage] - validate_job_dependencies!(name, job) if job[:dependencies] - end - def validate_job_stage!(name, job) + return unless job[:stage] + unless job[:stage].is_a?(String) && job[:stage].in?(@stages) raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" end end def validate_job_dependencies!(name, job) - unless validate_array_of_strings(job[:dependencies]) - raise ValidationError, "#{name} job: dependencies parameter should be an array of strings" - end + return unless job[:dependencies] stage_index = @stages.index(job[:stage]) diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index dc0813a8c18..ace79d829f2 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -13,7 +13,7 @@ module Gitlab type stage when artifacts cache dependencies before_script after_script variables environment] - attributes :tags, :allow_failure, :when, :environment + attributes :tags, :allow_failure, :when, :environment, :dependencies validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -37,6 +37,8 @@ module Gitlab format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + + validates :dependencies, array_of_strings: true end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 5785b7e59fb..61490555ff5 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1239,7 +1239,7 @@ EOT config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") end end From dc7805cd999dc6c97c407cbb419d1bb1cc36a71f Mon Sep 17 00:00:00 2001 From: Butch Anton Date: Thu, 21 Jul 2016 16:49:40 +0000 Subject: [PATCH 073/198] Update start-using-git.md --- doc/gitlab-basics/start-using-git.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 89ce8bcc3e8..b61f436c1a4 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -120,3 +120,11 @@ You need to be in the created branch. git checkout NAME-OF-BRANCH git merge master ``` + +### Merge master branch with created branch +You need to be in the master branch. +``` +git checkout master +git merge NAME-OF-BRANCH +``` + From 5495166bf5c00e91a7d4b9aa15c3aed083dbc290 Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Mon, 25 Jul 2016 12:32:14 -0700 Subject: [PATCH 074/198] Clarify CI script needing to quote colons --- doc/ci/yaml/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ea3fff1596e..189ce652a66 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -379,6 +379,8 @@ job: - bundle exec rspec ``` +Sometimes, `script` commands will need to be wrapped in single or double quotes. For example, commands that contain a colon (`:`) need to be wrapped in quotes so that the YAML parser knows to interpret the whole thing as a string rather than a "key: value" pair. Be careful when using special characters (`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `\``). + ### stage `stage` allows to group build into different stages. Builds of the same `stage` From 105e51c1dc774f443a9097d6b4acbfae4047403c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 26 Jul 2016 14:25:42 -0500 Subject: [PATCH 075/198] Line tooltip up with icon --- app/assets/stylesheets/pages/commits.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0298577c494..cbc980f52ff 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -61,6 +61,10 @@ font-size: 0; } + .ci-status-link { + display: inline-block; + } + .btn-clipboard, .btn-transparent { padding-left: 0; padding-right: 0; From 274769978ca576f4aea14eff2e3ec6532e3bcccd Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Tue, 26 Jul 2016 16:45:14 +0200 Subject: [PATCH 076/198] Use fewer queries for CI charts --- lib/ci/charts.rb | 96 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 1d7126a432d..3decc3b1a26 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -1,5 +1,37 @@ module Ci module Charts + module DailyInterval + def grouped_count(query) + query. + group("DATE(#{Ci::Build.table_name}.created_at)"). + count(:created_at). + transform_keys { |date| date.strftime(@format) } + end + + def interval_step + @interval_step ||= 1.day + end + end + + module MonthlyInterval + def grouped_count(query) + if Gitlab::Database.postgresql? + query. + group("to_char(#{Ci::Build.table_name}.created_at, '01 Month YYYY')"). + count(:created_at). + transform_keys(&:squish) + else + query. + group("DATE_FORMAT(#{Ci::Build.table_name}.created_at, '01 %M %Y')"). + count(:created_at) + end + end + + def interval_step + @interval_step ||= 1.month + end + end + class Chart attr_reader :labels, :total, :success, :project, :build_times @@ -13,47 +45,59 @@ module Ci collect end - def push(from, to, format) - @labels << from.strftime(format) - @total << project.builds. - where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). - count(:all) - @success << project.builds. - where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). - success.count(:all) + def collect + query = project.builds. + where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", @to, @from) + + totals_count = grouped_count(query) + success_count = grouped_count(query.success) + + current = @from + while current < @to + label = current.strftime(@format) + + @labels << label + @total << (totals_count[label] || 0) + @success << (success_count[label] || 0) + + current += interval_step + end end end class YearChart < Chart - def collect - 13.times do |i| - start_month = (Date.today.years_ago(1) + i.month).beginning_of_month - end_month = start_month.end_of_month + include MonthlyInterval - push(start_month, end_month, "%d %B %Y") - end + def initialize(*) + @to = Date.today.end_of_month + @from = @to.years_ago(1).beginning_of_month + @format = '%d %B %Y' + + super end end class MonthChart < Chart - def collect - 30.times do |i| - start_day = Date.today - 30.days + i.days - end_day = Date.today - 30.days + i.day + 1.day + include DailyInterval - push(start_day, end_day, "%d %B") - end + def initialize(*) + @to = Date.today + @from = @to - 30.days + @format = '%d %B' + + super end end class WeekChart < Chart - def collect - 7.times do |i| - start_day = Date.today - 7.days + i.days - end_day = Date.today - 7.days + i.day + 1.day + include DailyInterval - push(start_day, end_day, "%d %B") - end + def initialize(*) + @to = Date.today + @from = @to - 7.days + @format = '%d %B' + + super end end From 3032e977170596eb1fd738211410fb2c57e7d81a Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Wed, 27 Jul 2016 15:44:09 +0000 Subject: [PATCH 077/198] Update README.md --- doc/ci/yaml/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 189ce652a66..01d71088543 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -379,7 +379,7 @@ job: - bundle exec rspec ``` -Sometimes, `script` commands will need to be wrapped in single or double quotes. For example, commands that contain a colon (`:`) need to be wrapped in quotes so that the YAML parser knows to interpret the whole thing as a string rather than a "key: value" pair. Be careful when using special characters (`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `\``). +Sometimes, `script` commands will need to be wrapped in single or double quotes. For example, commands that contain a colon (`:`) need to be wrapped in quotes so that the YAML parser knows to interpret the whole thing as a string rather than a "key: value" pair. Be careful when using special characters (`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``). ### stage From 6e50719ff8dde5b5d54eea5e8f6fa95c5d0c0c24 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 21 Jul 2016 17:16:23 +0100 Subject: [PATCH 078/198] Fixed artifacts expire date in FF --- app/assets/javascripts/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e135cb92a30..3d9b824d406 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -128,7 +128,7 @@ $date = $('.js-artifacts-remove'); if ($date.length) { date = $date.text(); - return $date.text($.timefor(new Date(date), ' ')); + return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' ')); } }; From 8a9fc2b67e5bb6aeabdffa1f91115ac7613a505a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 27 Jul 2016 15:35:53 -0700 Subject: [PATCH 079/198] Cache the commit author in RequestStore to avoid extra lookups in PostReceive In a PostReceive task with 697 commits (8.9 RC1 -> RC8), looking up the commit author takes about 10% of the time. Caching this information in RequestStore saves a few seconds from the overall processing time. Improves #18663 --- CHANGELOG | 1 + app/models/commit.rb | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5ff0cb42ccc..d2ed7d8708e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.11.0 (unreleased) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Fix CI status icon link underline (ClemMakesApps) + - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Limit git rev-list output count to one in forced push check diff --git a/app/models/commit.rb b/app/models/commit.rb index f80f1063406..6a0d32d406e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -178,7 +178,14 @@ class Commit end def author - @author ||= User.find_by_any_email(author_email.downcase) + key = "commit_author:#{author_email}" + + # nil is a valid value since no author may exist in the system + unless RequestStore.store.has_key?(key) + RequestStore.store[key] = User.find_by_any_email(author_email.downcase) + end + + @author ||= RequestStore.store[key] end def committer From d27e36f35af0c2850c5370a3348d30ce4bcf1a68 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 27 Jul 2016 16:42:38 -0700 Subject: [PATCH 080/198] Add specs for caching commit author --- app/models/commit.rb | 22 +++++++++++++++------- spec/models/commit_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 6a0d32d406e..486ad6714d9 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -178,14 +178,18 @@ class Commit end def author - key = "commit_author:#{author_email}" - - # nil is a valid value since no author may exist in the system - unless RequestStore.store.has_key?(key) - RequestStore.store[key] = User.find_by_any_email(author_email.downcase) + if RequestStore.active? + key = "commit_author:#{author_email.downcase}" + # nil is a valid value since no author may exist in the system + if RequestStore.store.has_key?(key) + @author = RequestStore.store[key] + else + @author = find_author_by_any_email + RequestStore.store[key] = @author + end + else + @author ||= find_author_by_any_email end - - @author ||= RequestStore.store[key] end def committer @@ -313,6 +317,10 @@ class Commit private + def find_author_by_any_email + User.find_by_any_email(author_email.downcase) + end + def repo_changes changes = { added: [], modified: [], removed: [] } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ec1544bf815..c3392ee7440 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -13,6 +13,26 @@ describe Commit, models: true do it { is_expected.to include_module(StaticModel) } end + describe '#author' do + it 'looks up the author in a case-insensitive way' do + user = create(:user, email: commit.author_email.upcase) + expect(commit.author).to eq(user) + end + + it 'caches the author' do + user = create(:user, email: commit.author_email) + expect(RequestStore).to receive(:active?).twice.and_return(true) + expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original + + expect(commit.author).to eq(user) + key = "commit_author:#{commit.author_email}" + expect(RequestStore.store[key]).to eq(user) + + expect(commit.author).to eq(user) + RequestStore.store.clear + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(commit.to_reference).to eq commit.id From f178d0c73f838b3fa83ed462c413c0c88ef9dbbc Mon Sep 17 00:00:00 2001 From: Fernando Derkoski Date: Thu, 28 Jul 2016 02:02:39 +0000 Subject: [PATCH 081/198] Changed grant_type=AUTHORIZATION_CODE for grant_type=authorization_code --- doc/api/oauth2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 31902e145f6..7ce89adc98b 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -35,7 +35,7 @@ Where REDIRECT_URI is the URL in your app where users will be sent after authori To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client: ``` -parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=AUTHORIZATION_CODE&redirect_uri=REDIRECT_URI' +parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' RestClient.post 'http://localhost:3000/oauth/token', parameters # The response will be From 32d8aa6d5e1a6dbcd861a4e5baa24c3a873b57fe Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 28 Jul 2016 16:28:12 +0200 Subject: [PATCH 082/198] fix repo hooks missing on import fix spec and added changelog --- CHANGELOG | 1 + lib/gitlab/import_export/command_line_util.rb | 8 ++++++++ lib/gitlab/import_export/repo_restorer.rb | 12 +++++++++++- .../projects/import_export/import_file_spec.rb | 3 ++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 288adccbba2..b5f1b1a56b0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.11.0 (unreleased) - Change requests_profiles resource constraint to catch virtually any file v 8.10.3 (unreleased) + - Fix hooks missing on imported GitLab projects v 8.10.2 - User can now search branches by name. !5144 diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 5dd0e34c18e..e522a0fc8f6 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -17,6 +17,10 @@ module Gitlab execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path})) end + def git_restore_hooks + execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args) + end + private def tar_with_options(archive:, dir:, options:) @@ -45,6 +49,10 @@ module Gitlab FileUtils.copy_entry(source, destination) true end + + def repository_storage_paths_args + Gitlab.config.repositories.storages.values + end end end end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index f84de652a57..6d9379acf25 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -14,7 +14,7 @@ module Gitlab FileUtils.mkdir_p(path_to_repo) - git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) + git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks rescue => e @shared.error(e) false @@ -29,6 +29,16 @@ module Gitlab def path_to_repo @project.repository.path_to_repo end + + def repo_restore_hooks + return true if wiki? + + git_restore_hooks + end + + def wiki? + @project.class.name == 'ProjectWiki' + end end end end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 2d1e3bbebe5..7835e1678ad 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -8,6 +8,7 @@ feature 'project import', feature: true, js: true do let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } let(:project) { Project.last } + let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) } background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) @@ -37,7 +38,7 @@ feature 'project import', feature: true, js: true do expect(project).not_to be_nil expect(project.issues).not_to be_empty expect(project.merge_requests).not_to be_empty - expect(project.repo_exists?).to be true + expect(project_hook).to exist expect(wiki_exists?).to be true expect(project.import_status).to eq('finished') end From d00679d54f2bbaab9d6963c3b871a03ab8a3d7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Wed, 27 Jul 2016 11:26:29 -0400 Subject: [PATCH 083/198] Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 +- app/models/repository.rb | 31 +++++++------ app/services/delete_branch_service.rb | 2 +- app/services/delete_tag_service.rb | 2 +- app/services/git_tag_push_service.rb | 4 +- app/views/projects/branches/_commit.html.haml | 2 +- .../issues/_related_branches.html.haml | 4 +- spec/models/repository_spec.rb | 45 ++++++++++--------- .../_related_branches.html.haml_spec.rb | 21 +++++++++ 11 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 spec/views/projects/issues/_related_branches.html.haml_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 833a55e44f9..68f813af113 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 8.11.0 (unreleased) - Limit git rev-list output count to one in forced push check - Clean up unused routes (Josef Strzibny) - Add green outline to New Branch button. !5447 (winniehell) + - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects - Retrieve rendered HTML from cache in one request - Fix renaming repository when name contains invalid chararacters under project settings - Nokogiri's various parsing methods are now instrumented diff --git a/Gemfile b/Gemfile index 071277de068..a7d5e0e3e89 100644 --- a/Gemfile +++ b/Gemfile @@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.3.2' +gem 'gitlab_git', '~> 10.4.1' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 670578dec6d..150a98bb7d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.3.2) + gitlab_git (10.4.1) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -870,7 +870,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.3.2) + gitlab_git (~> 10.4.1) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) diff --git a/app/models/repository.rb b/app/models/repository.rb index af65e5b20ec..bac37483c47 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -70,7 +70,12 @@ class Repository def commit(ref = 'HEAD') return nil unless exists? - commit = Gitlab::Git::Commit.find(raw_repository, ref) + commit = + if ref.is_a?(Gitlab::Git::Commit) + ref + else + Gitlab::Git::Commit.find(raw_repository, ref) + end commit = ::Commit.new(commit, @project) if commit commit rescue Rugged::OdbError @@ -158,7 +163,7 @@ class Repository before_remove_branch branch = find_branch(branch_name) - oldrev = branch.try(:target) + oldrev = branch.try(:target).try(:id) newrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name @@ -259,10 +264,10 @@ class Repository # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes number_commits_behind = raw_repository. - count_commits_between(branch.target, root_ref_hash) + count_commits_between(branch.target.sha, root_ref_hash) number_commits_ahead = raw_repository. - count_commits_between(root_ref_hash, branch.target) + count_commits_between(root_ref_hash, branch.target.sha) { behind: number_commits_behind, ahead: number_commits_ahead } end @@ -688,9 +693,7 @@ class Repository end def local_branches - @local_branches ||= rugged.branches.each(:local).map do |branch| - Gitlab::Git::Branch.new(branch.name, branch.target) - end + @local_branches ||= raw_repository.local_branches end alias_method :branches, :local_branches @@ -831,7 +834,7 @@ class Repository end def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha revert_tree_id ||= check_revert_content(commit, base_branch) return false unless revert_tree_id @@ -848,7 +851,7 @@ class Repository end def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) return false unless cherry_pick_tree_id @@ -869,7 +872,7 @@ class Repository end def check_revert_content(commit, base_branch) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha args = [commit.id, source_sha] args << { mainline: 1 } if commit.merge_commit? @@ -883,7 +886,7 @@ class Repository end def check_cherry_pick_content(commit, base_branch) - source_sha = find_branch(base_branch).target + source_sha = find_branch(base_branch).target.sha args = [commit.id, source_sha] args << 1 if commit.merge_commit? @@ -974,7 +977,7 @@ class Repository was_empty = empty? if !was_empty && target_branch - oldrev = target_branch.target + oldrev = target_branch.target.id end # Make commit @@ -994,7 +997,7 @@ class Repository after_create_branch else # Update head - current_head = find_branch(branch).target + current_head = find_branch(branch).target.id # Make sure target branch was not changed during pre-receive hook if current_head == oldrev @@ -1052,7 +1055,7 @@ class Repository end def tags_sorted_by_committed_date - tags.sort_by { |tag| commit(tag.target).committed_date } + tags.sort_by { |tag| tag.target.committed_date } end def keep_around_ref_name(sha) diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 332c55581a1..87f066edb6f 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -40,6 +40,6 @@ class DeleteBranchService < BaseService def build_push_data(branch) Gitlab::PushDataBuilder - .build(project, current_user, branch.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index 1e41fbe34b6..32e0eed6b63 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -34,6 +34,6 @@ class DeleteTagService < BaseService def build_push_data(tag) Gitlab::PushDataBuilder - .build(project, current_user, tag.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 58573078048..969530c4fdc 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -26,8 +26,8 @@ class GitTagPushService < BaseService unless Gitlab::Git.blank_ref?(params[:newrev]) tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) - - if tag && tag.target == params[:newrev] + + if tag && tag.object_sha == params[:newrev] commit = project.commit(tag.target) commits = [commit].compact message = tag.message diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index 9fe65cbb104..d54c76ff9c8 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -1,5 +1,5 @@ .branch-commit - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace" + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace" · %span.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index c6fc499a7b8..6ea9f612d13 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -4,8 +4,8 @@ %ul.unstyled-list - @related_branches.each do |branch| %li - - sha = @project.repository.find_branch(branch).target - - pipeline = @project.pipeline(sha, branch) if sha + - target = @project.repository.find_branch(branch).target + - pipeline = @project.pipeline(target.sha, branch) if target - if pipeline %span.related-branch-ci-status = render_pipeline_status(pipeline) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 5bc1bd9a930..cce15538b93 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -50,8 +50,9 @@ describe Repository, models: true do double_first = double(committed_date: Time.now) double_last = double(committed_date: Time.now - 1.second) - allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first) - allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last) + allow(tag_a).to receive(:target).and_return(double_first) + allow(tag_b).to receive(:target).and_return(double_last) + allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } @@ -64,8 +65,9 @@ describe Repository, models: true do double_first = double(committed_date: Time.now - 1.second) double_last = double(committed_date: Time.now) - allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last) - allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first) + allow(tag_a).to receive(:target).and_return(double_last) + allow(tag_b).to receive(:target).and_return(double_first) + allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } @@ -381,9 +383,13 @@ describe Repository, models: true do end describe '#rm_branch' do + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:blank_sha) { '0000000000000000000000000000000000000000' } + context 'when pre hooks were successful' do it 'should run without errors' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) + expect_any_instance_of(GitHooksService).to receive(:execute). + with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -418,10 +424,13 @@ describe Repository, models: true do end describe '#commit_with_hooks' do + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + context 'when pre hooks were successful' do before do expect_any_instance_of(GitHooksService).to receive(:execute). - and_return(true) + with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature'). + and_yield.and_return(true) end it 'should run without errors' do @@ -435,6 +444,14 @@ describe Repository, models: true do repository.commit_with_hooks(user, 'feature') { sample_commit.id } end + + context "when the branch wasn't empty" do + it 'updates the head' do + expect(repository.find_branch('feature').target.id).to eq(old_rev) + repository.commit_with_hooks(user, 'feature') { sample_commit.id } + expect(repository.find_branch('feature').target.id).to eq(sample_commit.id) + end + end end context 'when pre hooks failed' do @@ -1198,17 +1215,6 @@ describe Repository, models: true do end end - describe '#local_branches' do - it 'returns the local branches' do - masterrev = repository.find_branch('master').target - create_remote_branch('joe', 'remote_branch', masterrev) - repository.add_branch(user, 'local_branch', masterrev) - - expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) - expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) - end - end - describe "#keep_around" do it "does not fail if we attempt to reference bad commit" do expect(repository.kept_around?('abc1234')).to be_falsey @@ -1236,9 +1242,4 @@ describe Repository, models: true do File.delete(path) end end - - def create_remote_branch(remote_name, branch_name, target) - rugged = repository.rugged - rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) - end end diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb new file mode 100644 index 00000000000..78af61f15a7 --- /dev/null +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'projects/issues/_related_branches' do + include Devise::TestHelpers + + let(:project) { create(:project) } + let(:branch) { project.repository.find_branch('feature') } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') } + + before do + assign(:project, project) + assign(:related_branches, ['feature']) + + render + end + + it 'shows the related branches with their build status' do + expect(rendered).to match('feature') + expect(rendered).to have_css('.related-branch-ci-status') + end +end From d49e6f355010a31a080da75789b46603989925ad Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 11:51:52 -0500 Subject: [PATCH 084/198] Add pipeline icon to admin builds; position warning icon after sha --- app/views/admin/builds/_build.html.haml | 7 ++++--- app/views/projects/ci/builds/_build.html.haml | 13 +++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index ce818c30c30..6d1ccf32b56 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -11,16 +11,17 @@ - else %span.build-link ##{build.id} - - if build.stuck? - %i.fa.fa-warning.text-warning - - if build.ref + .icon-container + = build.tag? ? icon('tag') : icon('code-fork') = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none = custom_icon("icon_commit") = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id" + - if build.stuck? + %i.fa.fa-warning.text-warning .label-container - if build.tags.any? diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index a3114771a42..91081435220 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,13 +13,6 @@ - else %span ##{build.id} - - if build.stuck? - .icon-container - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - - if defined?(retried) && retried - .icon-container - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - - if defined?(ref) && ref - if build.ref .icon-container @@ -33,6 +26,11 @@ - if defined?(commit_sha) && commit_sha = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" + - if build.stuck? + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + .label-container - if build.tags.any? - build.tags.each do |tag| @@ -47,7 +45,6 @@ - if build.manual? %span.label.label-info manual - - if defined?(runner) && runner %td - if build.try(:runner) From d286c624defd0549642c787ccd6782ee3e5ff42f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 12:59:35 -0500 Subject: [PATCH 085/198] Adjust min-widths of pipelines and builds tables --- app/assets/stylesheets/pages/pipelines.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index c58e2ffe7f5..7411c1c4499 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -18,6 +18,10 @@ .btn { margin: 4px; } + + .table.builds { + min-width: 1200px; + } } .content-list { @@ -35,7 +39,7 @@ } .table.builds { - min-width: 1200px; + min-width: 900px; &.pipeline { min-width: 650px; From 59797acd85037e9f491d298ac37743897ca2d6c2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 13:29:32 -0500 Subject: [PATCH 086/198] Add icon container --- app/views/admin/builds/_build.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 6d1ccf32b56..352adbedee4 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -17,7 +17,8 @@ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none - = custom_icon("icon_commit") + .icon-container + = custom_icon("icon_commit") = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id" - if build.stuck? From cebda439ea88d9868902120b1d20ff304610d9fd Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 17:31:53 -0500 Subject: [PATCH 087/198] Decrease icon container width to help fit all pipeline commit info on two lines --- app/assets/stylesheets/pages/pipelines.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7411c1c4499..21919fe4d73 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -132,7 +132,7 @@ .icon-container { display: inline-block; text-align: right; - width: 20px; + width: 15px; .fa { position: relative; From 3f1422a629a6866bc4cf164c00479eedb891ef8a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 28 Jul 2016 11:48:14 -0500 Subject: [PATCH 088/198] Add CI configuration button on project page --- CHANGELOG | 1 + app/views/projects/show.html.haml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 8759bb04dec..d2112fe2156 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.11.0 (unreleased) - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed - Add commit stats in commit api. !5517 (dixpac) + - Add CI configuration button on project page - Make error pages responsive (Takuya Noguchi) - Change requests_profiles resource constraint to catch virtually any file diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index dd1cf680cfa..cd0cd923ddb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -43,6 +43,10 @@ %li = link_to 'Contribution guide', contribution_guide_path(@project) + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', project_environments_path(@project) + - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog %li.missing From afca25ed8abddf4f9f3767fbc26a990a6e61dc8b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 28 Jul 2016 12:06:15 -0500 Subject: [PATCH 089/198] Link configuration button to .gitlab-ci.yml --- app/helpers/projects_helper.rb | 4 ++++ app/views/projects/show.html.haml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a733dff1579..505545fbabb 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -263,6 +263,10 @@ module ProjectsHelper filename_path(project, :version) end + def ci_configuration_path(project) + filename_path(project, :gitlab_ci_yml) + end + def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } namespace_project_wiki_path(proj.namespace, proj, page, url_params) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cd0cd923ddb..a666d07e9eb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -45,7 +45,7 @@ - if @repository.gitlab_ci_yml %li - = link_to 'CI configuration', project_environments_path(@project) + = link_to 'CI configuration', ci_configuration_path(@project) - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog From 08bac551494d92bc096859144a7a631b987ac35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 28 Jul 2016 16:37:03 -0400 Subject: [PATCH 090/198] Fix failing CommitController spec --- spec/controllers/projects/commit_controller_spec.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 3001d32e719..df902da86f8 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -24,15 +24,6 @@ describe Projects::CommitController do get :show, params.merge(extra_params) end - let(:project) { create(:project) } - - before do - user = create(:user) - project.team << [user, :master] - - sign_in(user) - end - context 'with valid id' do it 'responds with 200' do go(id: commit.id) From a71db022932b92e83977f81c3d94899ba2f7baeb Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Wed, 27 Jul 2016 17:43:36 +0100 Subject: [PATCH 091/198] Added es6 regex to teaspoon matchers, still doesnt fix it, problem with sprockets-es6 --- spec/teaspoon_env.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb index 69b2b9b6d5b..1497a4eb710 100644 --- a/spec/teaspoon_env.rb +++ b/spec/teaspoon_env.rb @@ -38,7 +38,7 @@ Teaspoon.configure do |config| # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. - suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}" + suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee,js.es6,es6}" # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. # suite.javascripts = [] From 42352d4b367d9d432bd92838171260d4559ba42e Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Thu, 28 Jul 2016 16:34:32 +0100 Subject: [PATCH 092/198] Removed coffee matchers --- spec/teaspoon_env.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb index 1497a4eb710..1a3bbb9c8cc 100644 --- a/spec/teaspoon_env.rb +++ b/spec/teaspoon_env.rb @@ -38,7 +38,7 @@ Teaspoon.configure do |config| # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. - suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee,js.es6,es6}" + suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}" # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. # suite.javascripts = [] From f1e46d1e63faf63f1dc9960c5f28d5260dfc84db Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 5 Jul 2016 12:29:15 +0530 Subject: [PATCH 093/198] Add a series of migrations changing the model-level design of protected branch access levels. 1. Remove the `developers_can_push` and `developers_can_merge` boolean columns. 2. Add two new tables, `protected_branches_push_access`, and `protected_branches_merge_access`. Each row of these 'access' tables is linked to a protected branch, and uses a `access_level` column to figure out settings for the protected branch. 3. The `access_level` column is intended to be used with rails' `enum`, with `:masters` at index 0 and `:developers` at index 1. 4. Doing it this way has a few advantages: - Cleaner path to planned EE features where a protected branch is accessible only by certain users or groups. - Rails' `enum` doesn't allow a declaration like this due to the duplicates. This approach doesn't have this problem. enum can_be_pushed_by: [:masters, :developers] enum can_be_merged_by: [:masters, :developers] --- ...4938_add_protected_branches_push_access.rb | 15 +++++++++ ...952_add_protected_branches_merge_access.rb | 15 +++++++++ ...erge_to_protected_branches_merge_access.rb | 33 +++++++++++++++++++ ..._push_to_protected_branches_push_access.rb | 33 +++++++++++++++++++ ...lopers_can_push_from_protected_branches.rb | 21 ++++++++++++ ...opers_can_merge_from_protected_branches.rb | 21 ++++++++++++ db/schema.rb | 26 ++++++++++++--- 7 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20160705054938_add_protected_branches_push_access.rb create mode 100644 db/migrate/20160705054952_add_protected_branches_merge_access.rb create mode 100644 db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb create mode 100644 db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb create mode 100644 db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb create mode 100644 db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb new file mode 100644 index 00000000000..512d99e4823 --- /dev/null +++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb @@ -0,0 +1,15 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddProtectedBranchesPushAccess < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + create_table :protected_branch_push_access_levels do |t| + t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false + t.integer :access_level, default: 0, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb new file mode 100644 index 00000000000..9f82c0a8aa3 --- /dev/null +++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb @@ -0,0 +1,15 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddProtectedBranchesMergeAccess < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + create_table :protected_branch_merge_access_levels do |t| + t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false + t.integer :access_level, default: 0, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb new file mode 100644 index 00000000000..20ca9c3a488 --- /dev/null +++ b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def up + execute <<-HEREDOC + INSERT into protected_branch_merge_access_levels (protected_branch_id, access_level, created_at, updated_at) + SELECT id, (CASE WHEN developers_can_merge THEN 1 ELSE 0 END), now(), now() + FROM protected_branches + HEREDOC + end + + def down + execute <<-HEREDOC + UPDATE protected_branches SET developers_can_merge = TRUE + WHERE id IN (SELECT protected_branch_id FROM protected_branch_merge_access_levels + WHERE access_level = 1); + HEREDOC + end +end diff --git a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb new file mode 100644 index 00000000000..498fb393d61 --- /dev/null +++ b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MoveFromDevelopersCanPushToProtectedBranchesPushAccess < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def up + execute <<-HEREDOC + INSERT into protected_branch_push_access_levels (protected_branch_id, access_level, created_at, updated_at) + SELECT id, (CASE WHEN developers_can_push THEN 1 ELSE 0 END), now(), now() + FROM protected_branches + HEREDOC + end + + def down + execute <<-HEREDOC + UPDATE protected_branches SET developers_can_push = TRUE + WHERE id IN (SELECT protected_branch_id FROM protected_branch_push_access_levels + WHERE access_level = 1); + HEREDOC + end +end diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb new file mode 100644 index 00000000000..1e9977cfa6e --- /dev/null +++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDevelopersCanPushFromProtectedBranches < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + remove_column :protected_branches, :developers_can_push, :boolean + end +end diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb new file mode 100644 index 00000000000..43d02fbaed6 --- /dev/null +++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDevelopersCanMergeFromProtectedBranches < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + remove_column :protected_branches, :developers_can_merge, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 15cee55a7bf..7a5eded8e02 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -867,13 +867,29 @@ ActiveRecord::Schema.define(version: 20160722221922) do add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree + create_table "protected_branch_merge_access_levels", force: :cascade do |t| + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree + + create_table "protected_branch_push_access_levels", force: :cascade do |t| + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree + create_table "protected_branches", force: :cascade do |t| - t.integer "project_id", null: false - t.string "name", null: false + t.integer "project_id", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "developers_can_push", default: false, null: false - t.boolean "developers_can_merge", default: false, null: false end add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree @@ -1136,5 +1152,7 @@ ActiveRecord::Schema.define(version: 20160722221922) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "personal_access_tokens", "users" + add_foreign_key "protected_branch_merge_access_levels", "protected_branches" + add_foreign_key "protected_branch_push_access_levels", "protected_branches" add_foreign_key "u2f_registrations", "users" end From 21bece443d5f871680a3d7649c2d16861035196d Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 5 Jul 2016 13:10:42 +0530 Subject: [PATCH 094/198] Add models for the protected branch access levels. - And hook up their associations. --- app/models/protected_branch.rb | 3 +++ app/models/protected_branch/merge_access_level.rb | 3 +++ app/models/protected_branch/push_access_level.rb | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 app/models/protected_branch/merge_access_level.rb create mode 100644 app/models/protected_branch/push_access_level.rb diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index b7011d7afdf..a411cb417e2 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -5,6 +5,9 @@ class ProtectedBranch < ActiveRecord::Base validates :name, presence: true validates :project, presence: true + has_one :merge_access_level + has_one :push_access_level + def commit project.commit(self.name) end diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb new file mode 100644 index 00000000000..78cec5bf566 --- /dev/null +++ b/app/models/protected_branch/merge_access_level.rb @@ -0,0 +1,3 @@ +class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base + belongs_to :protected_branch +end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb new file mode 100644 index 00000000000..d53c4c391e3 --- /dev/null +++ b/app/models/protected_branch/push_access_level.rb @@ -0,0 +1,3 @@ +class ProtectedBranch::PushAccessLevel < ActiveRecord::Base + belongs_to :protected_branch +end From 134fe5af83167f95205a080f7932452de7d77496 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 7 Jul 2016 13:06:28 +0530 Subject: [PATCH 095/198] Use the `{Push,Merge}AccessLevel` models in the UI. 1. Improve error handling while creating protected branches. 2. Modify coffeescript code so that the "Developers can *" checkboxes send a '1' or '0' even when using AJAX. This lets us keep the backend code simpler. 3. Use services for both creating and updating protected branches. Destruction is taken care of with `dependent: :destroy` --- .../projects/protected_branches_controller.rb | 24 +++++++++++++------ app/models/protected_branch.rb | 12 ++++++++-- .../protected_branch/merge_access_level.rb | 2 ++ .../protected_branch/push_access_level.rb | 2 ++ .../protected_branches/base_service.rb | 17 +++++++++++++ .../protected_branches/create_service.rb | 19 +++++++++++++++ .../protected_branches/update_service.rb | 21 ++++++++++++++++ 7 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 app/services/protected_branches/base_service.rb create mode 100644 app/services/protected_branches/create_service.rb create mode 100644 app/services/protected_branches/update_service.rb diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 10dca47fded..fdbe0044d3c 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -3,19 +3,23 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController before_action :require_non_empty_project before_action :authorize_admin_project! before_action :load_protected_branch, only: [:show, :update, :destroy] + before_action :load_protected_branches, only: [:index, :create] layout "project_settings" def index - @protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }) end def create - @project.protected_branches.create(protected_branch_params) - redirect_to namespace_project_protected_branches_path(@project.namespace, - @project) + service = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params) + if service.execute + redirect_to namespace_project_protected_branches_path(@project.namespace, @project) + else + @protected_branch = service.protected_branch + render :index + end end def show @@ -23,13 +27,15 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController end def update - if @protected_branch && @protected_branch.update_attributes(protected_branch_params) + service = ProtectedBranches::UpdateService.new(@project, current_user, params[:id], protected_branch_params) + + if service.execute respond_to do |format| - format.json { render json: @protected_branch, status: :ok } + format.json { render json: service.protected_branch, status: :ok } end else respond_to do |format| - format.json { render json: @protected_branch.errors, status: :unprocessable_entity } + format.json { render json: service.protected_branch.errors, status: :unprocessable_entity } end end end @@ -52,4 +58,8 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def protected_branch_params params.require(:protected_branch).permit(:name, :developers_can_push, :developers_can_merge) end + + def load_protected_branches + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) + end end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index a411cb417e2..b0fde6c6c1b 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -5,13 +5,21 @@ class ProtectedBranch < ActiveRecord::Base validates :name, presence: true validates :project, presence: true - has_one :merge_access_level - has_one :push_access_level + has_one :merge_access_level, dependent: :destroy + has_one :push_access_level, dependent: :destroy def commit project.commit(self.name) end + def developers_can_push + self.push_access_level && self.push_access_level.developers? + end + + def developers_can_merge + self.merge_access_level && self.merge_access_level.developers? + end + # Returns all protected branches that match the given branch name. # This realizes all records from the scope built up so far, and does # _not_ return a relation. diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 78cec5bf566..cfaa9c166fe 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,3 +1,5 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base belongs_to :protected_branch + + enum access_level: [:masters, :developers] end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index d53c4c391e3..4345dc4ede4 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,3 +1,5 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base belongs_to :protected_branch + + enum access_level: [:masters, :developers] end diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb new file mode 100644 index 00000000000..d4be8698a5f --- /dev/null +++ b/app/services/protected_branches/base_service.rb @@ -0,0 +1,17 @@ +module ProtectedBranches + class BaseService < ::BaseService + def set_access_levels! + if params[:developers_can_push] == '0' + @protected_branch.push_access_level.masters! + elsif params[:developers_can_push] == '1' + @protected_branch.push_access_level.developers! + end + + if params[:developers_can_merge] == '0' + @protected_branch.merge_access_level.masters! + elsif params[:developers_can_merge] == '1' + @protected_branch.merge_access_level.developers! + end + end + end +end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb new file mode 100644 index 00000000000..743f7bd2ce1 --- /dev/null +++ b/app/services/protected_branches/create_service.rb @@ -0,0 +1,19 @@ +class ProtectedBranches::CreateService < BaseService + attr_reader :protected_branch + + def execute + ProtectedBranch.transaction do + @protected_branch = project.protected_branches.new(name: params[:name]) + @protected_branch.save! + + @protected_branch.create_push_access_level! + @protected_branch.create_merge_access_level! + + set_access_levels! + end + + true + rescue ActiveRecord::RecordInvalid + false + end +end diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb new file mode 100644 index 00000000000..ed59b06b79a --- /dev/null +++ b/app/services/protected_branches/update_service.rb @@ -0,0 +1,21 @@ +module ProtectedBranches + class UpdateService < BaseService + attr_reader :protected_branch + + def initialize(project, current_user, id, params = {}) + super(project, current_user, params) + @id = id + end + + def execute + ProtectedBranch.transaction do + @protected_branch = ProtectedBranch.find(@id) + set_access_levels! + end + + true + rescue ActiveRecord::RecordInvalid + false + end + end +end From f8a04e15371815ad39e2c66056db4ab0439555f4 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 7 Jul 2016 13:21:13 +0530 Subject: [PATCH 096/198] Add seeds for protected branches. --- db/fixtures/development/16_protected_branches.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 db/fixtures/development/16_protected_branches.rb diff --git a/db/fixtures/development/16_protected_branches.rb b/db/fixtures/development/16_protected_branches.rb new file mode 100644 index 00000000000..103c7f9445c --- /dev/null +++ b/db/fixtures/development/16_protected_branches.rb @@ -0,0 +1,12 @@ +Gitlab::Seeder.quiet do + admin_user = User.find(1) + + Project.all.each do |project| + params = { + name: 'master' + } + + ProtectedBranches::CreateService.new(project, admin_user, params).execute + print '.' + end +end From ab6096c17261605d835a4a8edae21f31d90026df Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 7 Jul 2016 16:18:07 +0530 Subject: [PATCH 097/198] Add "No One Can Push" to the protected branches UI. 1. Move to dropdowns instead of checkboxes. One each for "Allowed to Push" and "Allowed to Merge" 2. Refactor the `ProtectedBranches` coffeescript class into `ProtectedBranchesAccessSelect`. 3. Modify the backend to accept the new parameters. --- ...protected_branches_access_select.js.coffee | 39 +++++++++++++++++++ app/assets/stylesheets/pages/projects.scss | 5 ++- .../projects/protected_branches_controller.rb | 2 +- .../protected_branch/push_access_level.rb | 2 +- .../protected_branches/base_service.rb | 20 ++++++---- .../protected_branches/create_service.rb | 28 ++++++------- .../_branches_list.html.haml | 37 +++++++++--------- .../_protected_branch.html.haml | 6 ++- 8 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 app/assets/javascripts/protected_branches_access_select.js.coffee diff --git a/app/assets/javascripts/protected_branches_access_select.js.coffee b/app/assets/javascripts/protected_branches_access_select.js.coffee new file mode 100644 index 00000000000..b472ff7ec27 --- /dev/null +++ b/app/assets/javascripts/protected_branches_access_select.js.coffee @@ -0,0 +1,39 @@ +class @ProtectedBranchesAccessSelect + constructor: () -> + $(".allowed-to-merge").each (i, element) => + fieldName = $(element).data('field-name') + $(element).glDropdown + data: [{id: 'developers', text: 'Developers'}, {id: 'masters', text: 'Masters'}] + selectable: true + fieldName: fieldName + clicked: _.partial(@onSelect, element) + + $(".allowed-to-push").each (i, element) => + fieldName = $(element).data('field-name') + $(element).glDropdown + data: [{id: 'no_one', text: 'No one'}, + {id: 'developers', text: 'Developers'}, + {id: 'masters', text: 'Masters'}] + selectable: true + fieldName: fieldName + clicked: _.partial(@onSelect, element) + + + onSelect: (dropdown, selected, element, e) => + $(dropdown).find('.dropdown-toggle-text').text(selected.text) + $.ajax + type: "PATCH" + url: $(dropdown).data('url') + dataType: "json" + data: + id: $(dropdown).data('id') + protected_branch: + "#{$(dropdown).data('type')}": selected.id + + success: -> + row = $(e.target) + row.closest('tr').effect('highlight') + + error: -> + new Flash("Failed to update branch!", "alert") + diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index cc3aef5199e..c6e73dd1f39 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -664,11 +664,14 @@ pre.light-well { .protected-branches-list { a { color: $gl-gray; - font-weight: 600; &:hover { color: $gl-link-color; } + + &.is-active { + font-weight: 600; + } } } diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index fdbe0044d3c..126358bfe77 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -56,7 +56,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController end def protected_branch_params - params.require(:protected_branch).permit(:name, :developers_can_push, :developers_can_merge) + params.require(:protected_branch).permit(:name, :allowed_to_push, :allowed_to_merge) end def load_protected_branches diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 4345dc4ede4..8861632c055 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,5 +1,5 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base belongs_to :protected_branch - enum access_level: [:masters, :developers] + enum access_level: [:masters, :developers, :no_one] end diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb index d4be8698a5f..3a7c35327fe 100644 --- a/app/services/protected_branches/base_service.rb +++ b/app/services/protected_branches/base_service.rb @@ -1,16 +1,20 @@ module ProtectedBranches class BaseService < ::BaseService def set_access_levels! - if params[:developers_can_push] == '0' - @protected_branch.push_access_level.masters! - elsif params[:developers_can_push] == '1' - @protected_branch.push_access_level.developers! + case params[:allowed_to_merge] + when 'masters' + @protected_branch.merge_access_level.masters! + when 'developers' + @protected_branch.merge_access_level.developers! end - if params[:developers_can_merge] == '0' - @protected_branch.merge_access_level.masters! - elsif params[:developers_can_merge] == '1' - @protected_branch.merge_access_level.developers! + case params[:allowed_to_push] + when 'masters' + @protected_branch.push_access_level.masters! + when 'developers' + @protected_branch.push_access_level.developers! + when 'no_one' + @protected_branch.push_access_level.no_one! end end end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index 743f7bd2ce1..ab462f3054e 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -1,19 +1,21 @@ -class ProtectedBranches::CreateService < BaseService - attr_reader :protected_branch +module ProtectedBranches + class CreateService < BaseService + attr_reader :protected_branch - def execute - ProtectedBranch.transaction do - @protected_branch = project.protected_branches.new(name: params[:name]) - @protected_branch.save! + def execute + ProtectedBranch.transaction do + @protected_branch = project.protected_branches.new(name: params[:name]) + @protected_branch.save! - @protected_branch.create_push_access_level! - @protected_branch.create_merge_access_level! + @protected_branch.create_push_access_level! + @protected_branch.create_merge_access_level! - set_access_levels! + set_access_levels! + end + + true + rescue ActiveRecord::RecordInvalid + false end - - true - rescue ActiveRecord::RecordInvalid - false end end diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 720d67dff7c..5f9f2992dca 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -5,24 +5,25 @@ No branches are protected, protect a branch with the form above. - else - can_admin_project = can?(current_user, :admin_project, @project) - .table-responsive - %table.table.protected-branches-list - %colgroup - %col{ width: "20%" } - %col{ width: "30%" } - %col{ width: "25%" } - %col{ width: "25%" } + + %table.table.protected-branches-list + %colgroup + %col{ width: "20%" } + %col{ width: "30%" } + %col{ width: "25%" } + %col{ width: "25%" } + %thead + %tr + %th Branch + %th Last commit + %th Allowed to Merge + %th Allowed to Push - if can_admin_project - %col - %thead - %tr - %th Protected Branch - %th Commit - %th Developers Can Push - %th Developers Can Merge - - if can_admin_project - %th - %tbody - = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + %th + %tbody + = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } = paginate @protected_branches, theme: 'gitlab' + +:javascript + new ProtectedBranchesAccessSelect(); diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 7fda7f96047..ceae182e82c 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -15,9 +15,11 @@ - else (branch was removed from repository) %td - = check_box_tag("developers_can_push", protected_branch.id, protected_branch.developers_can_push, data: { url: url }) + = hidden_field_tag "allowed_to_merge_#{branch.id}", branch.merge_access_level.access_level + = dropdown_tag(branch.merge_access_level.access_level.humanize, options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_merge_#{branch.id}", url: @url, id: branch.id, type: "allowed_to_merge" }}) %td - = check_box_tag("developers_can_merge", protected_branch.id, protected_branch.developers_can_merge, data: { url: url }) + = hidden_field_tag "allowed_to_push_#{branch.id}", branch.push_access_level.access_level + = dropdown_tag(branch.push_access_level.access_level.humanize, options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_push_#{branch.id}", url: @url, id: branch.id, type: "allowed_to_push" }}) - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" From 828f6eb6e50e6193fad9dbdd95d9dd56506e4064 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 8 Jul 2016 11:45:02 +0530 Subject: [PATCH 098/198] Enforce "No One Can Push" during git operations. 1. The crux of this change is in `UserAccess`, which looks through all the access levels, asking each if the user has access to push/merge for the current project. 2. Update the `protected_branches` factory to create access levels as necessary. 3. Fix and augment `user_access` and `git_access` specs. --- .../protected_branch/merge_access_level.rb | 9 +++ .../protected_branch/push_access_level.rb | 11 +++ lib/gitlab/user_access.rb | 10 ++- spec/factories/protected_branches.rb | 17 ++++ spec/lib/gitlab/git_access_spec.rb | 80 +++++++++++-------- spec/lib/gitlab/user_access_spec.rb | 4 +- 6 files changed, 90 insertions(+), 41 deletions(-) diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index cfaa9c166fe..2d13d8c8381 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,5 +1,14 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base belongs_to :protected_branch + delegate :project, to: :protected_branch enum access_level: [:masters, :developers] + + def check_access(user) + if masters? + user.can?(:push_code, project) if project.team.master?(user) + elsif developers? + user.can?(:push_code, project) if (project.team.master?(user) || project.team.developer?(user)) + end + end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 8861632c055..5a4a33556ce 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,5 +1,16 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base belongs_to :protected_branch + delegate :project, to: :protected_branch enum access_level: [:masters, :developers, :no_one] + + def check_access(user) + if masters? + user.can?(:push_code, project) if project.team.master?(user) + elsif developers? + user.can?(:push_code, project) if (project.team.master?(user) || project.team.developer?(user)) + elsif no_one? + false + end + end end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index c0f85e9b3a8..3a69027368f 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -29,8 +29,9 @@ module Gitlab def can_push_to_branch?(ref) return false unless user - if project.protected_branch?(ref) && !project.developers_can_push_to_protected_branch?(ref) - user.can?(:push_code_to_protected_branches, project) + if project.protected_branch?(ref) + access_levels = project.protected_branches.matching(ref).map(&:push_access_level) + access_levels.any? { |access_level| access_level.check_access(user) } else user.can?(:push_code, project) end @@ -39,8 +40,9 @@ module Gitlab def can_merge_to_branch?(ref) return false unless user - if project.protected_branch?(ref) && !project.developers_can_merge_to_protected_branch?(ref) - user.can?(:push_code_to_protected_branches, project) + if project.protected_branch?(ref) + access_levels = project.protected_branches.matching(ref).map(&:merge_access_level) + access_levels.any? { |access_level| access_level.check_access(user) } else user.can?(:push_code, project) end diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb index 28ed8078157..24a9b78f0c2 100644 --- a/spec/factories/protected_branches.rb +++ b/spec/factories/protected_branches.rb @@ -2,5 +2,22 @@ FactoryGirl.define do factory :protected_branch do name project + + after(:create) do |protected_branch| + protected_branch.create_push_access_level!(access_level: :masters) + protected_branch.create_merge_access_level!(access_level: :masters) + end + + trait :developers_can_push do + after(:create) { |protected_branch| protected_branch.push_access_level.developers! } + end + + trait :developers_can_merge do + after(:create) { |protected_branch| protected_branch.merge_access_level.developers! } + end + + trait :no_one_can_push do + after(:create) { |protected_branch| protected_branch.push_access_level.no_one! } + end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index ae064a878b0..324bb500025 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -217,29 +217,32 @@ describe Gitlab::GitAccess, lib: true do run_permission_checks(permissions_matrix) end - context "when 'developers can push' is turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_push: true, project: project) } + context "when developers are allowed to push into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end - context "when 'developers can merge' is turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_merge: true, project: project) } + context "developers are allowed to merge into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) } context "when a merge request exists for the given source/target branch" do context "when the merge request is in progress" do before do - create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) + create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', + state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) end - run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: true })) + context "when the merge request is not in progress" do + before do + create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', in_progress_merge_commit_sha: nil) + end + + run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) + end end - context "when the merge request is not in progress" do - before do - create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', in_progress_merge_commit_sha: nil) - end - + context "when a merge request does not exist for the given source/target branch" do run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) end end @@ -249,44 +252,51 @@ describe Gitlab::GitAccess, lib: true do end end - context "when 'developers can merge' and 'developers can push' are turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_merge: true, developers_can_push: true, project: project) } + context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end end - describe 'deploy key permissions' do - let(:key) { create(:deploy_key) } - let(:actor) { key } + context "when no one is allowed to push to the #{protected_branch_name} protected branch" do + before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } - context 'push code' do - subject { access.check('git-receive-pack') } + run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) + end + end - context 'when project is authorized' do - before { key.projects << project } + describe 'deploy key permissions' do + let(:key) { create(:deploy_key) } + let(:actor) { key } + + context 'push code' do + subject { access.check('git-receive-pack') } + + context 'when project is authorized' do + before { key.projects << project } + + it { expect(subject).not_to be_allowed } + end + + context 'when unauthorized' do + context 'to public project' do + let(:project) { create(:project, :public) } it { expect(subject).not_to be_allowed } end - context 'when unauthorized' do - context 'to public project' do - let(:project) { create(:project, :public) } + context 'to internal project' do + let(:project) { create(:project, :internal) } - it { expect(subject).not_to be_allowed } - end + it { expect(subject).not_to be_allowed } + end - context 'to internal project' do - let(:project) { create(:project, :internal) } + context 'to private project' do + let(:project) { create(:project, :internal) } - it { expect(subject).not_to be_allowed } - end - - context 'to private project' do - let(:project) { create(:project, :internal) } - - it { expect(subject).not_to be_allowed } - end + it { expect(subject).not_to be_allowed } end end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index aa9ec243498..5bb095366fa 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::UserAccess, lib: true do describe 'push to protected branch if allowed for developers' do before do - @branch = create :protected_branch, project: project, developers_can_push: true + @branch = create :protected_branch, :developers_can_push, project: project end it 'returns true if user is a master' do @@ -65,7 +65,7 @@ describe Gitlab::UserAccess, lib: true do describe 'merge to protected branch if allowed for developers' do before do - @branch = create :protected_branch, project: project, developers_can_merge: true + @branch = create :protected_branch, :developers_can_merge, project: project end it 'returns true if user is a master' do From 12387b4d2c6abbe1de2fc6b0776207d9135c29f0 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 8 Jul 2016 13:00:38 +0530 Subject: [PATCH 099/198] Allow setting "Allowed To Push/Merge" while creating a protected branch. 1. Reuse the same dropdown component that we used for updating these settings (`ProtectedBranchesAccessSelect`). Have it accept options for the parent container (so we can control the elements it sees) and whether or not to save changes via AJAX (we need this for update, but not create). 2. Change the "Developers" option to "Developers + Masters", which is clearer. 3. Remove `developers_can_push` and `developers_can_merge` from the model, since they're not needed anymore. --- ...protected_branches_access_select.js.coffee | 37 ++++++++++--------- app/assets/stylesheets/pages/projects.scss | 11 ++++++ app/models/protected_branch.rb | 8 ++-- .../_branches_list.html.haml | 2 +- .../_protected_branch.html.haml | 8 ++-- .../protected_branches/index.html.haml | 29 ++++++++++----- 6 files changed, 58 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/protected_branches_access_select.js.coffee b/app/assets/javascripts/protected_branches_access_select.js.coffee index b472ff7ec27..7c6f2f9f38e 100644 --- a/app/assets/javascripts/protected_branches_access_select.js.coffee +++ b/app/assets/javascripts/protected_branches_access_select.js.coffee @@ -1,18 +1,18 @@ class @ProtectedBranchesAccessSelect - constructor: () -> - $(".allowed-to-merge").each (i, element) => + constructor: (@container, @saveOnSelect) -> + @container.find(".allowed-to-merge").each (i, element) => fieldName = $(element).data('field-name') $(element).glDropdown - data: [{id: 'developers', text: 'Developers'}, {id: 'masters', text: 'Masters'}] + data: [{id: 'developers', text: 'Developers + Masters'}, {id: 'masters', text: 'Masters'}] selectable: true fieldName: fieldName clicked: _.partial(@onSelect, element) - $(".allowed-to-push").each (i, element) => + @container.find(".allowed-to-push").each (i, element) => fieldName = $(element).data('field-name') $(element).glDropdown data: [{id: 'no_one', text: 'No one'}, - {id: 'developers', text: 'Developers'}, + {id: 'developers', text: 'Developers + Masters'}, {id: 'masters', text: 'Masters'}] selectable: true fieldName: fieldName @@ -21,19 +21,20 @@ class @ProtectedBranchesAccessSelect onSelect: (dropdown, selected, element, e) => $(dropdown).find('.dropdown-toggle-text').text(selected.text) - $.ajax - type: "PATCH" - url: $(dropdown).data('url') - dataType: "json" - data: - id: $(dropdown).data('id') - protected_branch: - "#{$(dropdown).data('type')}": selected.id + if @saveOnSelect + $.ajax + type: "PATCH" + url: $(dropdown).data('url') + dataType: "json" + data: + id: $(dropdown).data('id') + protected_branch: + "#{$(dropdown).data('type')}": selected.id - success: -> - row = $(e.target) - row.closest('tr').effect('highlight') + success: -> + row = $(e.target) + row.closest('tr').effect('highlight') - error: -> - new Flash("Failed to update branch!", "alert") + error: -> + new Flash("Failed to update branch!", "alert") diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c6e73dd1f39..4409477916f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -661,6 +661,17 @@ pre.light-well { } } +.new_protected_branch { + .dropdown { + display: inline; + margin-left: 15px; + } + + label { + min-width: 120px; + } +} + .protected-branches-list { a { color: $gl-gray; diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index b0fde6c6c1b..c0bee72b4d7 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -12,12 +12,12 @@ class ProtectedBranch < ActiveRecord::Base project.commit(self.name) end - def developers_can_push - self.push_access_level && self.push_access_level.developers? + def allowed_to_push + self.push_access_level && self.push_access_level.access_level end - def developers_can_merge - self.merge_access_level && self.merge_access_level.developers? + def allowed_to_merge + self.merge_access_level && self.merge_access_level.access_level end # Returns all protected branches that match the given branch name. diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 5f9f2992dca..9240f1cf92d 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -26,4 +26,4 @@ = paginate @protected_branches, theme: 'gitlab' :javascript - new ProtectedBranchesAccessSelect(); + new ProtectedBranchesAccessSelect($(".protected-branches-list"), true); diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index ceae182e82c..96533b141af 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -15,11 +15,11 @@ - else (branch was removed from repository) %td - = hidden_field_tag "allowed_to_merge_#{branch.id}", branch.merge_access_level.access_level - = dropdown_tag(branch.merge_access_level.access_level.humanize, options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_merge_#{branch.id}", url: @url, id: branch.id, type: "allowed_to_merge" }}) + = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level + = dropdown_tag(protected_branch.merge_access_level.access_level.humanize, options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_merge" }}) %td - = hidden_field_tag "allowed_to_push_#{branch.id}", branch.push_access_level.access_level - = dropdown_tag(branch.push_access_level.access_level.humanize, options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_push_#{branch.id}", url: @url, id: branch.id, type: "allowed_to_push" }}) + = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level + = dropdown_tag(protected_branch.push_access_level.access_level.humanize, options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_push" }}) - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 950df740bbc..cd87f978fb2 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -32,19 +32,28 @@ are supported. .form-group - = f.check_box :developers_can_push, class: "pull-left" - .prepend-left-20 - = f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0" - %p.light.append-bottom-0 - Allow developers to push to this branch + .prepend-left-10 + = f.hidden_field :allowed_to_merge + = f.label :allowed_to_merge, "Allowed to Merge: ", class: "label-light append-bottom-0" + = dropdown_tag("", + options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "protected_branch[allowed_to_merge]" }}) .form-group - = f.check_box :developers_can_merge, class: "pull-left" - .prepend-left-20 - = f.label :developers_can_merge, "Developers can merge", class: "label-light append-bottom-0" - %p.light.append-bottom-0 - Allow developers to accept merge requests to this branch + .prepend-left-10 + = f.hidden_field :allowed_to_push + = f.label :allowed_to_push, "Allowed to Push: ", class: "label-light append-bottom-0" + = dropdown_tag("", + options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "protected_branch[allowed_to_push]" }}) + + = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true %hr = render "branches_list" + +:javascript + new ProtectedBranchesAccessSelect($(".new_protected_branch"), false); From 9fa661472e5e1e2edc91032a6093a3516974e27e Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 8 Jul 2016 14:18:50 +0530 Subject: [PATCH 100/198] Update protected branches spec to work with the `select`s. 1. Get the existing spec passing. 2. Add specs for all the access control options, both while creating and updating protected branches. 3. Show a flash notice when updating protected branches, primarily so the spec knows when the update is done. --- ...protected_branches_access_select.js.coffee | 1 + .../_protected_branch.html.haml | 8 +- spec/features/protected_branches_spec.rb | 75 +++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/protected_branches_access_select.js.coffee b/app/assets/javascripts/protected_branches_access_select.js.coffee index 7c6f2f9f38e..6df11146ba9 100644 --- a/app/assets/javascripts/protected_branches_access_select.js.coffee +++ b/app/assets/javascripts/protected_branches_access_select.js.coffee @@ -34,6 +34,7 @@ class @ProtectedBranchesAccessSelect success: -> row = $(e.target) row.closest('tr').effect('highlight') + new Flash("Updated protected branch!", "notice") error: -> new Flash("Failed to update branch!", "alert") diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 96533b141af..89d606d9e20 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -16,10 +16,14 @@ (branch was removed from repository) %td = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level - = dropdown_tag(protected_branch.merge_access_level.access_level.humanize, options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_merge" }}) + = dropdown_tag(protected_branch.merge_access_level.access_level.humanize, + options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', + data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_merge" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level - = dropdown_tag(protected_branch.push_access_level.access_level.humanize, options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_push" }}) + = dropdown_tag(protected_branch.push_access_level.access_level.humanize, + options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', + data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_push" }}) - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index d94dee0c797..087e3677169 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -81,4 +81,79 @@ feature 'Projected Branches', feature: true, js: true do end end end + + describe "access control" do + [ + ['developers', 'Developers + Masters'], + ['masters', 'Masters'], + ['no_one', 'No one'] + ].each do |access_type_id, access_type_name| + it "allows creating protected branches that #{access_type_name} can push to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + find(".allowed-to-push").click + click_on access_type_name + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.allowed_to_push).to eq(access_type_id) + end + + # This spec fails on PhantomJS versions below 2.0, which don't support `PATCH` requests. + # https://github.com/ariya/phantomjs/issues/11384 + it "allows updating protected branches so that #{access_type_name} can push to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".allowed-to-push").click + within('.dropdown-menu.push') { click_on access_type_name } + end + + expect(page).to have_content "Updated protected branch" + expect(ProtectedBranch.last.allowed_to_push).to eq(access_type_id) + end + end + + [ + ['developers', 'Developers + Masters'], + ['masters', 'Masters'] + ].each do |access_type_id, access_type_name| + it "allows creating protected branches that #{access_type_name} can merge to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + find(".allowed-to-merge").click + click_on access_type_name + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.allowed_to_merge).to eq(access_type_id) + end + + # This spec fails on PhantomJS versions below 2.0, which don't support `PATCH` requests. + # https://github.com/ariya/phantomjs/issues/11384 + it "allows updating protected branches so that #{access_type_name} can merge to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".allowed-to-merge").click + within('.dropdown-menu.merge') { click_on access_type_name } + end + + expect(page).to have_content "Updated protected branch" + expect(ProtectedBranch.last.allowed_to_merge).to eq(access_type_id) + end + end + end end From a9958ddc7c9d4c455d4c5459b7b83da1fab9ccb4 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 8 Jul 2016 14:45:29 +0530 Subject: [PATCH 101/198] Fix default branch protection. 1. So it works with the new data model for protected branch access levels. --- app/services/git_push_service.rb | 8 +++++--- .../protected_branches/create_service.rb | 2 +- .../protected_branches/update_service.rb | 2 +- spec/services/git_push_service_spec.rb | 17 ++++++++++++----- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index e02b50ff9a2..604737e6934 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -88,9 +88,11 @@ class GitPushService < BaseService # Set protection on the default branch if configured if current_application_settings.default_branch_protection != PROTECTION_NONE - developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false - developers_can_merge = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? true : false - @project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push, developers_can_merge: developers_can_merge }) + allowed_to_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? 'developers' : 'masters' + allowed_to_merge = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? 'developers' : 'masters' + + params = { name: @project.default_branch, allowed_to_push: allowed_to_push, allowed_to_merge: allowed_to_merge } + ProtectedBranches::CreateService.new(@project, current_user, params).execute end end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index ab462f3054e..212c2134638 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -1,5 +1,5 @@ module ProtectedBranches - class CreateService < BaseService + class CreateService < ProtectedBranches::BaseService attr_reader :protected_branch def execute diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index ed59b06b79a..4a2b1be9c93 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -1,5 +1,5 @@ module ProtectedBranches - class UpdateService < BaseService + class UpdateService < ProtectedBranches::BaseService attr_reader :protected_branch def initialize(project, current_user, id, params = {}) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 47c0580e0f0..663c270d61f 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -224,8 +224,10 @@ describe GitPushService, services: true do it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false, developers_can_merge: false }) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.allowed_to_push).to eq('masters') + expect(project.protected_branches.first.allowed_to_merge).to eq('masters') end it "when pushing a branch for the first time with default branch protection disabled" do @@ -233,8 +235,8 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).not_to receive(:create) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).to be_empty end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do @@ -242,9 +244,12 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true, developers_can_merge: false }) - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.last.allowed_to_push).to eq('developers') + expect(project.protected_branches.last.allowed_to_merge).to eq('masters') end it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do @@ -252,8 +257,10 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false, developers_can_merge: true }) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.allowed_to_push).to eq('masters') + expect(project.protected_branches.first.allowed_to_merge).to eq('developers') end it "when pushing new commits to existing branch" do From c647540c1010fd1e51bced1db90947aa00c83fa8 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 8 Jul 2016 14:46:13 +0530 Subject: [PATCH 102/198] Fix all specs related to changes in !5081. 1. Remove `Project#developers_can_push_to_protected_branch?` since it isn't used anymore. 2. Remove `Project#developers_can_merge_to_protected_branch?` since it isn't used anymore. --- app/models/project.rb | 8 ---- .../protected_branch/merge_access_level.rb | 2 +- .../protected_branch/push_access_level.rb | 2 +- features/steps/project/commits/branches.rb | 2 +- spec/lib/gitlab/git_access_spec.rb | 2 +- spec/models/project_spec.rb | 40 ------------------- 6 files changed, 4 insertions(+), 52 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index dc44a757b4b..7aecd7860c5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -874,14 +874,6 @@ class Project < ActiveRecord::Base ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end - def developers_can_push_to_protected_branch?(branch_name) - protected_branches.matching(branch_name).any?(&:developers_can_push) - end - - def developers_can_merge_to_protected_branch?(branch_name) - protected_branches.matching(branch_name).any?(&:developers_can_merge) - end - def forked? !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 2d13d8c8381..3d2a6971702 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -8,7 +8,7 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base if masters? user.can?(:push_code, project) if project.team.master?(user) elsif developers? - user.can?(:push_code, project) if (project.team.master?(user) || project.team.developer?(user)) + user.can?(:push_code, project) if project.team.master?(user) || project.team.developer?(user) end end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 5a4a33556ce..d446c1a03f0 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -8,7 +8,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base if masters? user.can?(:push_code, project) if project.team.master?(user) elsif developers? - user.can?(:push_code, project) if (project.team.master?(user) || project.team.developer?(user)) + user.can?(:push_code, project) if project.team.master?(user) || project.team.developer?(user) elsif no_one? false end diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 0a42931147d..4bfb7e92e99 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -25,7 +25,7 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step 'project "Shop" has protected branches' do project = Project.find_by(name: "Shop") - project.protected_branches.create(name: "stable") + create(:protected_branch, project: project, name: "stable") end step 'I click new branch link' do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 324bb500025..8d7497f76f5 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -230,7 +230,7 @@ describe Gitlab::GitAccess, lib: true do context "when the merge request is in progress" do before do create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', - state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) + state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) end context "when the merge request is not in progress" do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 72b8a4e25bd..e365e4e98b2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1095,46 +1095,6 @@ describe Project, models: true do end end - describe "#developers_can_push_to_protected_branch?" do - let(:project) { create(:empty_project) } - - context "when the branch matches a protected branch via direct match" do - it "returns true if 'Developers can Push' is turned on" do - create(:protected_branch, name: "production", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('production')).to be true - end - - it "returns false if 'Developers can Push' is turned off" do - create(:protected_branch, name: "production", project: project, developers_can_push: false) - - expect(project.developers_can_push_to_protected_branch?('production')).to be false - end - end - - context "when the branch matches a protected branch via wilcard match" do - it "returns true if 'Developers can Push' is turned on" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true - end - - it "returns false if 'Developers can Push' is turned off" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: false) - - expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false - end - end - - context "when the branch does not match a protected branch" do - it "returns false" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false - end - end - end - describe '#container_registry_path_with_namespace' do let(:project) { create(:empty_project, path: 'PROJECT') } From f2df2966aabc601dd1d6a6f9e75ead84db8a2765 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 14 Jul 2016 09:12:16 +0530 Subject: [PATCH 103/198] Humanize protected branches' access levels at one location. 1. The model now contains this humanization data, which is the once source of truth. 2. Previously, this was being listed out in the dropdown component as well. --- .../protected_branches_access_select.js.coffee | 6 ++---- .../projects/protected_branches_controller.rb | 4 +++- app/models/protected_branch/merge_access_level.rb | 11 +++++++++++ app/models/protected_branch/push_access_level.rb | 12 ++++++++++++ .../protected_branches/_protected_branch.html.haml | 4 ++-- spec/features/protected_branches_spec.rb | 11 ++--------- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/protected_branches_access_select.js.coffee b/app/assets/javascripts/protected_branches_access_select.js.coffee index 6df11146ba9..2c29513ae61 100644 --- a/app/assets/javascripts/protected_branches_access_select.js.coffee +++ b/app/assets/javascripts/protected_branches_access_select.js.coffee @@ -3,7 +3,7 @@ class @ProtectedBranchesAccessSelect @container.find(".allowed-to-merge").each (i, element) => fieldName = $(element).data('field-name') $(element).glDropdown - data: [{id: 'developers', text: 'Developers + Masters'}, {id: 'masters', text: 'Masters'}] + data: gon.merge_access_levels selectable: true fieldName: fieldName clicked: _.partial(@onSelect, element) @@ -11,9 +11,7 @@ class @ProtectedBranchesAccessSelect @container.find(".allowed-to-push").each (i, element) => fieldName = $(element).data('field-name') $(element).glDropdown - data: [{id: 'no_one', text: 'No one'}, - {id: 'developers', text: 'Developers + Masters'}, - {id: 'masters', text: 'Masters'}] + data: gon.push_access_levels selectable: true fieldName: fieldName clicked: _.partial(@onSelect, element) diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 126358bfe77..ddf1824ccb9 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -9,7 +9,9 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def index @protected_branch = @project.protected_branches.new - gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }) + gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }, + push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } }, + merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } }) end def create diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 3d2a6971702..d536f816317 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -4,6 +4,13 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base enum access_level: [:masters, :developers] + def self.human_access_levels + { + "masters" => "Masters", + "developers" => "Developers + Masters" + }.with_indifferent_access + end + def check_access(user) if masters? user.can?(:push_code, project) if project.team.master?(user) @@ -11,4 +18,8 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base user.can?(:push_code, project) if project.team.master?(user) || project.team.developer?(user) end end + + def humanize + self.class.human_access_levels[self.access_level] + end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index d446c1a03f0..bb46b39b714 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -4,6 +4,14 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base enum access_level: [:masters, :developers, :no_one] + def self.human_access_levels + { + "masters" => "Masters", + "developers" => "Developers + Masters", + "no_one" => "No one" + }.with_indifferent_access + end + def check_access(user) if masters? user.can?(:push_code, project) if project.team.master?(user) @@ -13,4 +21,8 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base false end end + + def humanize + self.class.human_access_levels[self.access_level] + end end diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 89d606d9e20..e27dea8145d 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -16,12 +16,12 @@ (branch was removed from repository) %td = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level - = dropdown_tag(protected_branch.merge_access_level.access_level.humanize, + = dropdown_tag(protected_branch.merge_access_level.humanize, options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_merge" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level - = dropdown_tag(protected_branch.push_access_level.access_level.humanize, + = dropdown_tag(protected_branch.push_access_level.humanize, options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_push" }}) - if can_admin_project diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 087e3677169..553d1c70461 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -83,11 +83,7 @@ feature 'Projected Branches', feature: true, js: true do end describe "access control" do - [ - ['developers', 'Developers + Masters'], - ['masters', 'Masters'], - ['no_one', 'No one'] - ].each do |access_type_id, access_type_name| + ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do visit namespace_project_protected_branches_path(project.namespace, project) set_protected_branch_name('master') @@ -120,10 +116,7 @@ feature 'Projected Branches', feature: true, js: true do end end - [ - ['developers', 'Developers + Masters'], - ['masters', 'Masters'] - ].each do |access_type_id, access_type_name| + ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can merge to" do visit namespace_project_protected_branches_path(project.namespace, project) set_protected_branch_name('master') From 4d6dadc8f8af986a0792fb388775a174e76b0b7d Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 14 Jul 2016 09:24:59 +0530 Subject: [PATCH 104/198] Make specs compatible with PhantomJS versions < 2. 1. These versions of PhantomJS don't support `PATCH` requests, so we use a `POST` with `_method` set to `PATCH`. --- .../javascripts/protected_branches_access_select.js.coffee | 3 ++- spec/features/protected_branches_spec.rb | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/protected_branches_access_select.js.coffee b/app/assets/javascripts/protected_branches_access_select.js.coffee index 2c29513ae61..a4d9b6eb616 100644 --- a/app/assets/javascripts/protected_branches_access_select.js.coffee +++ b/app/assets/javascripts/protected_branches_access_select.js.coffee @@ -21,10 +21,11 @@ class @ProtectedBranchesAccessSelect $(dropdown).find('.dropdown-toggle-text').text(selected.text) if @saveOnSelect $.ajax - type: "PATCH" + type: "POST" url: $(dropdown).data('url') dataType: "json" data: + _method: 'PATCH' id: $(dropdown).data('id') protected_branch: "#{$(dropdown).data('type')}": selected.id diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 553d1c70461..d72b62a4962 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -97,8 +97,6 @@ feature 'Projected Branches', feature: true, js: true do expect(ProtectedBranch.last.allowed_to_push).to eq(access_type_id) end - # This spec fails on PhantomJS versions below 2.0, which don't support `PATCH` requests. - # https://github.com/ariya/phantomjs/issues/11384 it "allows updating protected branches so that #{access_type_name} can push to them" do visit namespace_project_protected_branches_path(project.namespace, project) set_protected_branch_name('master') @@ -130,8 +128,6 @@ feature 'Projected Branches', feature: true, js: true do expect(ProtectedBranch.last.allowed_to_merge).to eq(access_type_id) end - # This spec fails on PhantomJS versions below 2.0, which don't support `PATCH` requests. - # https://github.com/ariya/phantomjs/issues/11384 it "allows updating protected branches so that #{access_type_name} can merge to them" do visit namespace_project_protected_branches_path(project.namespace, project) set_protected_branch_name('master') From cc1cebdcc536244d97bdf6c767c2f1875c71cdf5 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 14 Jul 2016 11:46:13 +0530 Subject: [PATCH 105/198] Admins count as masters too. 1. In the context of protected branches. 2. Test this behaviour. --- app/models/project_team.rb | 8 +++++ .../protected_branch/merge_access_level.rb | 4 +-- .../protected_branch/push_access_level.rb | 4 +-- spec/lib/gitlab/git_access_spec.rb | 30 +++++++++++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index fdfaf052730..436d5bd2948 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -118,6 +118,14 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end + def master_or_greater?(user) + master?(user) || user.is_admin? + end + + def developer_or_greater?(user) + master_or_greater?(user) || developer?(user) + end + def member?(user, min_member_access = nil) member = !!find_member(user.id) diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index d536f816317..632e47b028f 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -13,9 +13,9 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base def check_access(user) if masters? - user.can?(:push_code, project) if project.team.master?(user) + user.can?(:push_code, project) if project.team.master_or_greater?(user) elsif developers? - user.can?(:push_code, project) if project.team.master?(user) || project.team.developer?(user) + user.can?(:push_code, project) if project.team.developer_or_greater?(user) end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index bb46b39b714..35d4ad93231 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -14,9 +14,9 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base def check_access(user) if masters? - user.can?(:push_code, project) if project.team.master?(user) + user.can?(:push_code, project) if project.team.master_or_greater?(user) elsif developers? - user.can?(:push_code, project) if project.team.master?(user) || project.team.developer?(user) + user.can?(:push_code, project) if project.team.developer_or_greater?(user) elsif no_one? false end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 8d7497f76f5..c6f03525aab 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -151,7 +151,13 @@ describe Gitlab::GitAccess, lib: true do def self.run_permission_checks(permissions_matrix) permissions_matrix.keys.each do |role| describe "#{role} access" do - before { project.team << [user, role] } + before do + if role == :admin + user.update_attribute(:admin, true) + else + project.team << [user, role] + end + end permissions_matrix[role].each do |action, allowed| context action do @@ -165,6 +171,17 @@ describe Gitlab::GitAccess, lib: true do end permissions_matrix = { + admin: { + push_new_branch: true, + push_master: true, + push_protected_branch: true, + push_remove_protected_branch: false, + push_tag: true, + push_new_tag: true, + push_all: true, + merge_into_protected_branch: true + }, + master: { push_new_branch: true, push_master: true, @@ -257,13 +274,14 @@ describe Gitlab::GitAccess, lib: true do run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end - end - context "when no one is allowed to push to the #{protected_branch_name} protected branch" do - before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } + context "when no one is allowed to push to the #{protected_branch_name} protected branch" do + before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } - run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, - master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) + run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) + end end end From 8e25ddc529e41d8244c9e7b4adcf54e071b8c318 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 14 Jul 2016 11:46:58 +0530 Subject: [PATCH 106/198] Add changelog entry. --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d555d23860d..c791776a891 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.11.0 (unreleased) - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes + - Add "No one can push" as an option for protected branches. !5081 - Limit git rev-list output count to one in forced push check - Clean up unused routes (Josef Strzibny) - Add green outline to New Branch button. !5447 (winniehell) @@ -126,6 +127,8 @@ v 8.10.0 - Allow to define manual actions/builds on Pipelines and Environments - Fix pagination when sorting by columns with lots of ties (like priority) - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times. !5020 + - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 + - Add "No one can push" as an option for protected branches. !5081 - Updated project header design - Issuable collapsed assignee tooltip is now the users name - Fix compare view not changing code view rendering style From b3a29b3180c4edda33d82fc3564bd4991831e06c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 19 Jul 2016 17:46:57 +0530 Subject: [PATCH 107/198] Favor labels like `Allowed to push` over `Allowed To Push`. - Based on feedback from @axil - http://docs.gitlab.com/ce/development/ui_guide.html#buttons --- .../projects/protected_branches/_branches_list.html.haml | 4 ++-- .../protected_branches/_protected_branch.html.haml | 4 ++-- app/views/projects/protected_branches/index.html.haml | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 9240f1cf92d..a6956c8e69f 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -16,8 +16,8 @@ %tr %th Branch %th Last commit - %th Allowed to Merge - %th Allowed to Push + %th Allowed to merge + %th Allowed to push - if can_admin_project %th %tbody diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index e27dea8145d..2fc6081e448 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -17,12 +17,12 @@ %td = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level = dropdown_tag(protected_branch.merge_access_level.humanize, - options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', + options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_merge" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level = dropdown_tag(protected_branch.push_access_level.humanize, - options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', + options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_push" }}) - if can_admin_project %td diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index cd87f978fb2..69caed7d979 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -34,18 +34,18 @@ .form-group .prepend-left-10 = f.hidden_field :allowed_to_merge - = f.label :allowed_to_merge, "Allowed to Merge: ", class: "label-light append-bottom-0" + = f.label :allowed_to_merge, "Allowed to merge: ", class: "label-light append-bottom-0" = dropdown_tag("", - options: { title: "Allowed To Merge", toggle_class: 'allowed-to-merge', + options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "protected_branch[allowed_to_merge]" }}) .form-group .prepend-left-10 = f.hidden_field :allowed_to_push - = f.label :allowed_to_push, "Allowed to Push: ", class: "label-light append-bottom-0" + = f.label :allowed_to_push, "Allowed to push: ", class: "label-light append-bottom-0" = dropdown_tag("", - options: { title: "Allowed To Push", toggle_class: 'allowed-to-push', + options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "protected_branch[allowed_to_push]" }}) From 7b2ad2d5b99d84fc2d2c11a654085afc02a05bb1 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 25 Jul 2016 14:42:52 +0530 Subject: [PATCH 108/198] Implement review comments from @dbalexandre. 1. Remove `master_or_greater?` and `developer_or_greater?` in favor of `max_member_access`, which is a lot nicer. 2. Remove a number of instances of `include Gitlab::Database::MigrationHelpers` in migrations that don't need this module. Also remove comments where not necessary. 3. Remove duplicate entry in CHANGELOG. 4. Move `ProtectedBranchAccessSelect` from Coffeescript to ES6. 5. Split the `set_access_levels!` method in two - one each for `merge` and `push` access levels. --- CHANGELOG | 1 - app/assets/javascripts/protected_branches.js | 35 ------------ ...protected_branches_access_select.js.coffee | 40 -------------- .../protected_branches_access_select.js.es6 | 53 +++++++++++++++++++ app/models/project_team.rb | 8 --- .../protected_branch/merge_access_level.rb | 14 +++-- .../protected_branch/push_access_level.rb | 17 +++--- .../protected_branches/base_service.rb | 9 ++++ ...4938_add_protected_branches_push_access.rb | 2 - ...952_add_protected_branches_merge_access.rb | 2 - ...erge_to_protected_branches_merge_access.rb | 13 ----- ..._push_to_protected_branches_push_access.rb | 13 ----- ...lopers_can_push_from_protected_branches.rb | 13 ----- ...opers_can_merge_from_protected_branches.rb | 13 ----- 14 files changed, 81 insertions(+), 152 deletions(-) delete mode 100644 app/assets/javascripts/protected_branches.js delete mode 100644 app/assets/javascripts/protected_branches_access_select.js.coffee create mode 100644 app/assets/javascripts/protected_branches_access_select.js.es6 diff --git a/CHANGELOG b/CHANGELOG index c791776a891..4f1da451df0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -127,7 +127,6 @@ v 8.10.0 - Allow to define manual actions/builds on Pipelines and Environments - Fix pagination when sorting by columns with lots of ties (like priority) - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times. !5020 - - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 - Add "No one can push" as an option for protected branches. !5081 - Updated project header design - Issuable collapsed assignee tooltip is now the users name diff --git a/app/assets/javascripts/protected_branches.js b/app/assets/javascripts/protected_branches.js deleted file mode 100644 index db21a19964d..00000000000 --- a/app/assets/javascripts/protected_branches.js +++ /dev/null @@ -1,35 +0,0 @@ -(function() { - $(function() { - return $(".protected-branches-list :checkbox").change(function(e) { - var can_push, id, name, obj, url; - name = $(this).attr("name"); - if (name === "developers_can_push" || name === "developers_can_merge") { - id = $(this).val(); - can_push = $(this).is(":checked"); - url = $(this).data("url"); - return $.ajax({ - type: "PATCH", - url: url, - dataType: "json", - data: { - id: id, - protected_branch: ( - obj = {}, - obj["" + name] = can_push, - obj - ) - }, - success: function() { - var row; - row = $(e.target); - return row.closest('tr').effect('highlight'); - }, - error: function() { - return new Flash("Failed to update branch!", "alert"); - } - }); - } - }); - }); - -}).call(this); diff --git a/app/assets/javascripts/protected_branches_access_select.js.coffee b/app/assets/javascripts/protected_branches_access_select.js.coffee deleted file mode 100644 index a4d9b6eb616..00000000000 --- a/app/assets/javascripts/protected_branches_access_select.js.coffee +++ /dev/null @@ -1,40 +0,0 @@ -class @ProtectedBranchesAccessSelect - constructor: (@container, @saveOnSelect) -> - @container.find(".allowed-to-merge").each (i, element) => - fieldName = $(element).data('field-name') - $(element).glDropdown - data: gon.merge_access_levels - selectable: true - fieldName: fieldName - clicked: _.partial(@onSelect, element) - - @container.find(".allowed-to-push").each (i, element) => - fieldName = $(element).data('field-name') - $(element).glDropdown - data: gon.push_access_levels - selectable: true - fieldName: fieldName - clicked: _.partial(@onSelect, element) - - - onSelect: (dropdown, selected, element, e) => - $(dropdown).find('.dropdown-toggle-text').text(selected.text) - if @saveOnSelect - $.ajax - type: "POST" - url: $(dropdown).data('url') - dataType: "json" - data: - _method: 'PATCH' - id: $(dropdown).data('id') - protected_branch: - "#{$(dropdown).data('type')}": selected.id - - success: -> - row = $(e.target) - row.closest('tr').effect('highlight') - new Flash("Updated protected branch!", "notice") - - error: -> - new Flash("Failed to update branch!", "alert") - diff --git a/app/assets/javascripts/protected_branches_access_select.js.es6 b/app/assets/javascripts/protected_branches_access_select.js.es6 new file mode 100644 index 00000000000..93b7d7755a7 --- /dev/null +++ b/app/assets/javascripts/protected_branches_access_select.js.es6 @@ -0,0 +1,53 @@ +class ProtectedBranchesAccessSelect { + constructor(container, saveOnSelect) { + this.container = container; + this.saveOnSelect = saveOnSelect; + + this.container.find(".allowed-to-merge").each((i, element) => { + var fieldName = $(element).data('field-name'); + return $(element).glDropdown({ + data: gon.merge_access_levels, + selectable: true, + fieldName: fieldName, + clicked: _.chain(this.onSelect).partial(element).bind(this).value() + }); + }); + + + this.container.find(".allowed-to-push").each((i, element) => { + var fieldName = $(element).data('field-name'); + return $(element).glDropdown({ + data: gon.push_access_levels, + selectable: true, + fieldName: fieldName, + clicked: _.chain(this.onSelect).partial(element).bind(this).value() + }); + }); + } + + onSelect(dropdown, selected, element, e) { + $(dropdown).find('.dropdown-toggle-text').text(selected.text); + if (this.saveOnSelect) { + return $.ajax({ + type: "POST", + url: $(dropdown).data('url'), + dataType: "json", + data: { + _method: 'PATCH', + id: $(dropdown).data('id'), + protected_branch: { + ["" + ($(dropdown).data('type'))]: selected.id + } + }, + success: function() { + var row; + row = $(e.target); + return row.closest('tr').effect('highlight'); + }, + error: function() { + return new Flash("Failed to update branch!", "alert"); + } + }); + } + } +} diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 436d5bd2948..fdfaf052730 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -118,14 +118,6 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end - def master_or_greater?(user) - master?(user) || user.is_admin? - end - - def developer_or_greater?(user) - master_or_greater?(user) || developer?(user) - end - def member?(user, min_member_access = nil) member = !!find_member(user.id) diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 632e47b028f..17a3a86c3e1 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -12,11 +12,15 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base end def check_access(user) - if masters? - user.can?(:push_code, project) if project.team.master_or_greater?(user) - elsif developers? - user.can?(:push_code, project) if project.team.developer_or_greater?(user) - end + return true if user.is_admin? + + min_member_access = if masters? + Gitlab::Access::MASTER + elsif developers? + Gitlab::Access::DEVELOPER + end + + project.team.max_member_access(user.id) >= min_member_access end def humanize diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 35d4ad93231..22096b13300 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -13,13 +13,16 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base end def check_access(user) - if masters? - user.can?(:push_code, project) if project.team.master_or_greater?(user) - elsif developers? - user.can?(:push_code, project) if project.team.developer_or_greater?(user) - elsif no_one? - false - end + return false if no_one? + return true if user.is_admin? + + min_member_access = if masters? + Gitlab::Access::MASTER + elsif developers? + Gitlab::Access::DEVELOPER + end + + project.team.max_member_access(user.id) >= min_member_access end def humanize diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb index 3a7c35327fe..f8741fcb3d5 100644 --- a/app/services/protected_branches/base_service.rb +++ b/app/services/protected_branches/base_service.rb @@ -1,13 +1,22 @@ module ProtectedBranches class BaseService < ::BaseService def set_access_levels! + set_merge_access_levels! + set_push_access_levels! + end + + protected + + def set_merge_access_levels! case params[:allowed_to_merge] when 'masters' @protected_branch.merge_access_level.masters! when 'developers' @protected_branch.merge_access_level.developers! end + end + def set_push_access_levels! case params[:allowed_to_push] when 'masters' @protected_branch.push_access_level.masters! diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb index 512d99e4823..3031574fe2a 100644 --- a/db/migrate/20160705054938_add_protected_branches_push_access.rb +++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb @@ -2,8 +2,6 @@ # for more information on how to write migrations for GitLab. class AddProtectedBranchesPushAccess < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - def change create_table :protected_branch_push_access_levels do |t| t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb index 9f82c0a8aa3..cf1cdb8b3b6 100644 --- a/db/migrate/20160705054952_add_protected_branches_merge_access.rb +++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb @@ -2,8 +2,6 @@ # for more information on how to write migrations for GitLab. class AddProtectedBranchesMergeAccess < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - def change create_table :protected_branch_merge_access_levels do |t| t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false diff --git a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb index 20ca9c3a488..c2b278ce673 100644 --- a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb +++ b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb @@ -2,19 +2,6 @@ # for more information on how to write migrations for GitLab. class MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def up execute <<-HEREDOC INSERT into protected_branch_merge_access_levels (protected_branch_id, access_level, created_at, updated_at) diff --git a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb index 498fb393d61..5bc70283f60 100644 --- a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb +++ b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb @@ -2,19 +2,6 @@ # for more information on how to write migrations for GitLab. class MoveFromDevelopersCanPushToProtectedBranchesPushAccess < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def up execute <<-HEREDOC INSERT into protected_branch_push_access_levels (protected_branch_id, access_level, created_at, updated_at) diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb index 1e9977cfa6e..ad6ad43686d 100644 --- a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb +++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb @@ -2,19 +2,6 @@ # for more information on how to write migrations for GitLab. class RemoveDevelopersCanPushFromProtectedBranches < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def change remove_column :protected_branches, :developers_can_push, :boolean end diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb index 43d02fbaed6..084914e423a 100644 --- a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb +++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb @@ -2,19 +2,6 @@ # for more information on how to write migrations for GitLab. class RemoveDevelopersCanMergeFromProtectedBranches < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def change remove_column :protected_branches, :developers_can_merge, :boolean end From a72d4491903ad4f6730565df6391667e8ba8b71f Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 25 Jul 2016 14:59:05 +0530 Subject: [PATCH 109/198] Remove duplicate specs from `git_access_spec` - Likely introduced during an improper conflict resolution. --- spec/lib/gitlab/git_access_spec.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index c6f03525aab..8447305a316 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -250,23 +250,21 @@ describe Gitlab::GitAccess, lib: true do state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) end - context "when the merge request is not in progress" do - before do - create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', in_progress_merge_commit_sha: nil) - end + run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: true })) + end - run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) + context "when the merge request is not in progress" do + before do + create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', in_progress_merge_commit_sha: nil) end + + run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) end context "when a merge request does not exist for the given source/target branch" do run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) end end - - context "when a merge request does not exist for the given source/target branch" do - run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) - end end context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do From 88fd401d599f94bc4ea572cd47afa7d12c484db2 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 25 Jul 2016 15:39:15 +0530 Subject: [PATCH 110/198] Implement review comments from @axil. 1. Align "Allowed to Merge" and "Allowed to Push" dropdowns. 2. Don't display a flash every time a protected branch is updated. Previously, we were using this so the test has something to hook onto before the assertion. Now we're using `wait_for_ajax` instead. --- .../protected_branches/index.html.haml | 26 +++++++++---------- spec/features/protected_branches_spec.rb | 6 +++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 69caed7d979..75c2063027a 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -32,22 +32,20 @@ are supported. .form-group - .prepend-left-10 - = f.hidden_field :allowed_to_merge - = f.label :allowed_to_merge, "Allowed to merge: ", class: "label-light append-bottom-0" - = dropdown_tag("", - options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', - dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[allowed_to_merge]" }}) + = f.hidden_field :allowed_to_merge + = f.label :allowed_to_merge, "Allowed to merge: ", class: "label-light append-bottom-0" + = dropdown_tag("", + options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "protected_branch[allowed_to_merge]" }}) .form-group - .prepend-left-10 - = f.hidden_field :allowed_to_push - = f.label :allowed_to_push, "Allowed to push: ", class: "label-light append-bottom-0" - = dropdown_tag("", - options: { title: "Allowed to push", toggle_class: 'allowed-to-push', - dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[allowed_to_push]" }}) + = f.hidden_field :allowed_to_push + = f.label :allowed_to_push, "Allowed to push: ", class: "label-light append-bottom-0" + = dropdown_tag("", + options: { title: "Allowed to push", toggle_class: 'allowed-to-push', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "protected_branch[allowed_to_push]" }}) = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index d72b62a4962..dac2bcf9efd 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Projected Branches', feature: true, js: true do + include WaitForAjax + let(:user) { create(:user, :admin) } let(:project) { create(:project) } @@ -109,7 +111,7 @@ feature 'Projected Branches', feature: true, js: true do within('.dropdown-menu.push') { click_on access_type_name } end - expect(page).to have_content "Updated protected branch" + wait_for_ajax expect(ProtectedBranch.last.allowed_to_push).to eq(access_type_id) end end @@ -140,7 +142,7 @@ feature 'Projected Branches', feature: true, js: true do within('.dropdown-menu.merge') { click_on access_type_name } end - expect(page).to have_content "Updated protected branch" + wait_for_ajax expect(ProtectedBranch.last.allowed_to_merge).to eq(access_type_id) end end From 01d190a84ad9b8e4a40cbdec8a55946bac38ab76 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 25 Jul 2016 20:14:53 +0530 Subject: [PATCH 111/198] Have the `branches` API work with the new protected branches data model. 1. The new data model moves from `developers_can_{push,merge}` to `allowed_to_{push,merge}`. 2. The API interface has not been changed. It still accepts `developers_can_push` and `developers_can_merge` as options. These attributes are inferred from the new data model. 3. Modify the protected branch create/update services to translate from the API interface to our current data model. --- .../protected_branches/base_service.rb | 34 +++++++++++++++++-- lib/api/branches.rb | 27 +++++++++------ lib/api/entities.rb | 6 ++-- spec/requests/api/branches_spec.rb | 2 +- 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb index f8741fcb3d5..a5896587ded 100644 --- a/app/services/protected_branches/base_service.rb +++ b/app/services/protected_branches/base_service.rb @@ -1,6 +1,15 @@ module ProtectedBranches class BaseService < ::BaseService + include API::Helpers + + def initialize(project, current_user, params = {}) + super(project, current_user, params) + @allowed_to_push = params[:allowed_to_push] + @allowed_to_merge = params[:allowed_to_merge] + end + def set_access_levels! + translate_api_params! set_merge_access_levels! set_push_access_levels! end @@ -8,7 +17,7 @@ module ProtectedBranches protected def set_merge_access_levels! - case params[:allowed_to_merge] + case @allowed_to_merge when 'masters' @protected_branch.merge_access_level.masters! when 'developers' @@ -17,7 +26,7 @@ module ProtectedBranches end def set_push_access_levels! - case params[:allowed_to_push] + case @allowed_to_push when 'masters' @protected_branch.push_access_level.masters! when 'developers' @@ -26,5 +35,26 @@ module ProtectedBranches @protected_branch.push_access_level.no_one! end end + + # The `branches` API still uses `developers_can_push` and `developers_can_merge`, + # which need to be translated to `allowed_to_push` and `allowed_to_merge`, + # respectively. + def translate_api_params! + @allowed_to_push ||= + case to_boolean(params[:developers_can_push]) + when true + 'developers' + when false + 'masters' + end + + @allowed_to_merge ||= + case to_boolean(params[:developers_can_merge]) + when true + 'developers' + when false + 'masters' + end + end end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 66b853eb342..4133a1f7a6b 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -35,6 +35,10 @@ module API # Protect a single branch # + # Note: The internal data model moved from `developers_can_{merge,push}` to `allowed_to_{merge,push}` + # in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility), + # but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`. + # # Parameters: # id (required) - The ID of a project # branch (required) - The name of the branch @@ -49,18 +53,19 @@ module API @branch = user_project.repository.find_branch(params[:branch]) not_found!('Branch') unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) - developers_can_push = to_boolean(params[:developers_can_push]) - developers_can_merge = to_boolean(params[:developers_can_merge]) + protected_branch_params = { + name: @branch.name, + developers_can_push: params[:developers_can_push], + developers_can_merge: params[:developers_can_merge] + } - if protected_branch - protected_branch.developers_can_push = developers_can_push unless developers_can_push.nil? - protected_branch.developers_can_merge = developers_can_merge unless developers_can_merge.nil? - protected_branch.save - else - user_project.protected_branches.create(name: @branch.name, - developers_can_push: developers_can_push || false, - developers_can_merge: developers_can_merge || false) - end + service = if protected_branch + ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch.id, protected_branch_params) + else + ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params) + end + + service.execute present @branch, with: Entities::RepoBranch, project: user_project end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e76e7304674..e51bee5c846 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -126,11 +126,13 @@ module API end expose :developers_can_push do |repo_branch, options| - options[:project].developers_can_push_to_protected_branch? repo_branch.name + project = options[:project] + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.developers? } end expose :developers_can_merge do |repo_branch, options| - options[:project].developers_can_merge_to_protected_branch? repo_branch.name + project = options[:project] + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.developers? } end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 719da27f919..e8fd697965f 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -112,7 +112,7 @@ describe API::API, api: true do before do project.repository.add_branch(user, protected_branch, 'master') - create(:protected_branch, project: project, name: protected_branch, developers_can_push: true, developers_can_merge: true) + create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch) end it 'updates that a developer can push' do From 6d841eaadcbccfa4527bd892bf86fc8dbba19455 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 27 Jul 2016 10:14:38 +0530 Subject: [PATCH 112/198] Authorize user before creating/updating a protected branch. 1. This is a third line of defence (first in the view, second in the controller). 2. Duplicate the `API::Helpers.to_boolean` method in `BaseService`. The other alternative is to `include API::Helpers`, but this brings with it a number of other methods that might cause conflicts. 3. Return a 403 if authorization fails. --- app/services/protected_branches/base_service.rb | 13 ++++++++++--- app/services/protected_branches/create_service.rb | 2 ++ app/services/protected_branches/update_service.rb | 5 +++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb index a5896587ded..bdd175e8552 100644 --- a/app/services/protected_branches/base_service.rb +++ b/app/services/protected_branches/base_service.rb @@ -1,7 +1,5 @@ module ProtectedBranches class BaseService < ::BaseService - include API::Helpers - def initialize(project, current_user, params = {}) super(project, current_user, params) @allowed_to_push = params[:allowed_to_push] @@ -14,7 +12,7 @@ module ProtectedBranches set_push_access_levels! end - protected + private def set_merge_access_levels! case @allowed_to_merge @@ -56,5 +54,14 @@ module ProtectedBranches 'masters' end end + + protected + + def to_boolean(value) + return true if value =~ /^(true|t|yes|y|1|on)$/i + return false if value =~ /^(false|f|no|n|0|off)$/i + + nil + end end end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index 212c2134638..36019906416 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -3,6 +3,8 @@ module ProtectedBranches attr_reader :protected_branch def execute + raise Gitlab::Access::AccessDeniedError unless current_user.can?(:admin_project, project) + ProtectedBranch.transaction do @protected_branch = project.protected_branches.new(name: params[:name]) @protected_branch.save! diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index 4a2b1be9c93..58f2f774bae 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -4,12 +4,13 @@ module ProtectedBranches def initialize(project, current_user, id, params = {}) super(project, current_user, params) - @id = id + @protected_branch = ProtectedBranch.find(id) end def execute + raise Gitlab::Access::AccessDeniedError unless current_user.can?(:admin_project, project) + ProtectedBranch.transaction do - @protected_branch = ProtectedBranch.find(@id) set_access_levels! end From c93a895abc434b9b78aa7cf4d285ce309cfd868a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 27 Jul 2016 12:33:50 +0530 Subject: [PATCH 113/198] Fix `git_push_service_spec` 1. Caused by incorrect test setup. The user wasn't added to the project, so protected branch creation failed authorization. 2. Change setup for a different test (`Event.last` to `Event.find_by_action`) because our `project.team << ...` addition was causing a conflict. --- spec/services/git_push_service_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 663c270d61f..621eced83f6 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -7,6 +7,7 @@ describe GitPushService, services: true do let(:project) { create :project } before do + project.team << [user, :master] @blankrev = Gitlab::Git::BLANK_SHA @oldrev = sample_commit.parent_id @newrev = sample_commit.id @@ -172,7 +173,7 @@ describe GitPushService, services: true do describe "Push Event" do before do service = execute_service(project, user, @oldrev, @newrev, @ref ) - @event = Event.last + @event = Event.find_by_action(Event::PUSHED) @push_data = service.push_data end From 0a8aeb46dc187cc309ddbe23d8624f5d24b6218c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 29 Jul 2016 11:43:07 +0530 Subject: [PATCH 114/198] Use `Gitlab::Access` to protected branch access levels. 1. It makes sense to reuse these constants since we had them duplicated in the previous enum implementation. This also simplifies our `check_access` implementation, because we can use `project.team.max_member_access` directly. 2. Use `accepts_nested_attributes_for` to create push/merge access levels. This was a bit fiddly to set up, but this simplifies our code by quite a large amount. We can even get rid of `ProtectedBranches::BaseService`. 3. Move API handling back into the API (previously in `ProtectedBranches::BaseService#translate_api_params`. 4. The protected branch services now return a `ProtectedBranch` rather than `true/false`. 5. Run `load_protected_branches` on-demand in the `create` action, to prevent it being called unneccessarily. 6. "Masters" is pre-selected as the default option for "Allowed to Push" and "Allowed to Merge". 7. These changes were based on a review from @rymai in !5081. --- .../protected_branches_access_select.js.es6 | 18 +++-- .../projects/protected_branches_controller.rb | 31 +++++---- app/models/protected_branch.rb | 11 +-- .../protected_branch/merge_access_level.rb | 16 ++--- .../protected_branch/push_access_level.rb | 21 +++--- app/services/git_push_service.rb | 13 +++- .../protected_branches/base_service.rb | 67 ------------------- .../protected_branches/create_service.rb | 20 +++--- .../protected_branches/update_service.rb | 19 ++---- .../_branches_list.html.haml | 2 +- .../_protected_branch.html.haml | 4 +- .../protected_branches/index.html.haml | 14 ++-- ...4938_add_protected_branches_push_access.rb | 4 +- ...952_add_protected_branches_merge_access.rb | 4 +- db/schema.rb | 16 ++--- lib/api/branches.rb | 36 +++++++--- lib/api/entities.rb | 4 +- spec/factories/protected_branches.rb | 16 +++-- spec/features/protected_branches_spec.rb | 12 ++-- spec/services/git_push_service_spec.rb | 12 ++-- 20 files changed, 152 insertions(+), 188 deletions(-) delete mode 100644 app/services/protected_branches/base_service.rb diff --git a/app/assets/javascripts/protected_branches_access_select.js.es6 b/app/assets/javascripts/protected_branches_access_select.js.es6 index 93b7d7755a7..e98312bbf37 100644 --- a/app/assets/javascripts/protected_branches_access_select.js.es6 +++ b/app/assets/javascripts/protected_branches_access_select.js.es6 @@ -1,27 +1,35 @@ class ProtectedBranchesAccessSelect { - constructor(container, saveOnSelect) { + constructor(container, saveOnSelect, selectDefault) { this.container = container; this.saveOnSelect = saveOnSelect; this.container.find(".allowed-to-merge").each((i, element) => { var fieldName = $(element).data('field-name'); - return $(element).glDropdown({ + var dropdown = $(element).glDropdown({ data: gon.merge_access_levels, selectable: true, fieldName: fieldName, clicked: _.chain(this.onSelect).partial(element).bind(this).value() }); + + if (selectDefault) { + dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); + } }); this.container.find(".allowed-to-push").each((i, element) => { var fieldName = $(element).data('field-name'); - return $(element).glDropdown({ + var dropdown = $(element).glDropdown({ data: gon.push_access_levels, selectable: true, fieldName: fieldName, clicked: _.chain(this.onSelect).partial(element).bind(this).value() }); + + if (selectDefault) { + dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); + } }); } @@ -36,7 +44,9 @@ class ProtectedBranchesAccessSelect { _method: 'PATCH', id: $(dropdown).data('id'), protected_branch: { - ["" + ($(dropdown).data('type'))]: selected.id + ["" + ($(dropdown).data('type')) + "_attributes"]: { + "access_level": selected.id + } } }, success: function() { diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index ddf1824ccb9..d28ec6e2eac 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -3,23 +3,22 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController before_action :require_non_empty_project before_action :authorize_admin_project! before_action :load_protected_branch, only: [:show, :update, :destroy] - before_action :load_protected_branches, only: [:index, :create] + before_action :load_protected_branches, only: [:index] layout "project_settings" def index @protected_branch = @project.protected_branches.new - gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }, - push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } }, - merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } }) + load_protected_branches_gon_variables end def create - service = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params) - if service.execute + @protected_branch = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute + if @protected_branch.persisted? redirect_to namespace_project_protected_branches_path(@project.namespace, @project) else - @protected_branch = service.protected_branch + load_protected_branches + load_protected_branches_gon_variables render :index end end @@ -29,15 +28,15 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController end def update - service = ProtectedBranches::UpdateService.new(@project, current_user, params[:id], protected_branch_params) + @protected_branch = ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch) - if service.execute + if @protected_branch.valid? respond_to do |format| - format.json { render json: service.protected_branch, status: :ok } + format.json { render json: @protected_branch, status: :ok } end else respond_to do |format| - format.json { render json: service.protected_branch.errors, status: :unprocessable_entity } + format.json { render json: @protected_branch.errors, status: :unprocessable_entity } end end end @@ -58,10 +57,18 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController end def protected_branch_params - params.require(:protected_branch).permit(:name, :allowed_to_push, :allowed_to_merge) + params.require(:protected_branch).permit(:name, + merge_access_level_attributes: [:access_level], + push_access_level_attributes: [:access_level]) end def load_protected_branches @protected_branches = @project.protected_branches.order(:name).page(params[:page]) end + + def load_protected_branches_gon_variables + gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }, + push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } }, + merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } }) + end end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index c0bee72b4d7..226b3f54342 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -8,18 +8,13 @@ class ProtectedBranch < ActiveRecord::Base has_one :merge_access_level, dependent: :destroy has_one :push_access_level, dependent: :destroy + accepts_nested_attributes_for :push_access_level + accepts_nested_attributes_for :merge_access_level + def commit project.commit(self.name) end - def allowed_to_push - self.push_access_level && self.push_access_level.access_level - end - - def allowed_to_merge - self.merge_access_level && self.merge_access_level.access_level - end - # Returns all protected branches that match the given branch name. # This realizes all records from the scope built up so far, and does # _not_ return a relation. diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 17a3a86c3e1..25a6ca6a8ee 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -2,25 +2,19 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base belongs_to :protected_branch delegate :project, to: :protected_branch - enum access_level: [:masters, :developers] + validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER] } def self.human_access_levels { - "masters" => "Masters", - "developers" => "Developers + Masters" + Gitlab::Access::MASTER => "Masters", + Gitlab::Access::DEVELOPER => "Developers + Masters" }.with_indifferent_access end def check_access(user) return true if user.is_admin? - - min_member_access = if masters? - Gitlab::Access::MASTER - elsif developers? - Gitlab::Access::DEVELOPER - end - - project.team.max_member_access(user.id) >= min_member_access + project.team.max_member_access(user.id) >= access_level end def humanize diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 22096b13300..1999316aa26 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -2,27 +2,22 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base belongs_to :protected_branch delegate :project, to: :protected_branch - enum access_level: [:masters, :developers, :no_one] + validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS] } def self.human_access_levels { - "masters" => "Masters", - "developers" => "Developers + Masters", - "no_one" => "No one" + Gitlab::Access::MASTER => "Masters", + Gitlab::Access::DEVELOPER => "Developers + Masters", + Gitlab::Access::NO_ACCESS => "No one" }.with_indifferent_access end def check_access(user) - return false if no_one? + return false if access_level == Gitlab::Access::NO_ACCESS return true if user.is_admin? - - min_member_access = if masters? - Gitlab::Access::MASTER - elsif developers? - Gitlab::Access::DEVELOPER - end - - project.team.max_member_access(user.id) >= min_member_access + project.team.max_member_access(user.id) >= access_level end def humanize diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 604737e6934..3f6a177bf3a 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -88,10 +88,17 @@ class GitPushService < BaseService # Set protection on the default branch if configured if current_application_settings.default_branch_protection != PROTECTION_NONE - allowed_to_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? 'developers' : 'masters' - allowed_to_merge = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? 'developers' : 'masters' - params = { name: @project.default_branch, allowed_to_push: allowed_to_push, allowed_to_merge: allowed_to_merge } + params = { + name: @project.default_branch, + push_access_level_attributes: { + access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + }, + merge_access_level_attributes: { + access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + } + } + ProtectedBranches::CreateService.new(@project, current_user, params).execute end end diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb deleted file mode 100644 index bdd175e8552..00000000000 --- a/app/services/protected_branches/base_service.rb +++ /dev/null @@ -1,67 +0,0 @@ -module ProtectedBranches - class BaseService < ::BaseService - def initialize(project, current_user, params = {}) - super(project, current_user, params) - @allowed_to_push = params[:allowed_to_push] - @allowed_to_merge = params[:allowed_to_merge] - end - - def set_access_levels! - translate_api_params! - set_merge_access_levels! - set_push_access_levels! - end - - private - - def set_merge_access_levels! - case @allowed_to_merge - when 'masters' - @protected_branch.merge_access_level.masters! - when 'developers' - @protected_branch.merge_access_level.developers! - end - end - - def set_push_access_levels! - case @allowed_to_push - when 'masters' - @protected_branch.push_access_level.masters! - when 'developers' - @protected_branch.push_access_level.developers! - when 'no_one' - @protected_branch.push_access_level.no_one! - end - end - - # The `branches` API still uses `developers_can_push` and `developers_can_merge`, - # which need to be translated to `allowed_to_push` and `allowed_to_merge`, - # respectively. - def translate_api_params! - @allowed_to_push ||= - case to_boolean(params[:developers_can_push]) - when true - 'developers' - when false - 'masters' - end - - @allowed_to_merge ||= - case to_boolean(params[:developers_can_merge]) - when true - 'developers' - when false - 'masters' - end - end - - protected - - def to_boolean(value) - return true if value =~ /^(true|t|yes|y|1|on)$/i - return false if value =~ /^(false|f|no|n|0|off)$/i - - nil - end - end -end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index 36019906416..50f79f491ce 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -1,23 +1,27 @@ module ProtectedBranches - class CreateService < ProtectedBranches::BaseService + class CreateService < BaseService attr_reader :protected_branch def execute raise Gitlab::Access::AccessDeniedError unless current_user.can?(:admin_project, project) + protected_branch = project.protected_branches.new(params) + ProtectedBranch.transaction do - @protected_branch = project.protected_branches.new(name: params[:name]) - @protected_branch.save! + protected_branch.save! - @protected_branch.create_push_access_level! - @protected_branch.create_merge_access_level! + if protected_branch.push_access_level.blank? + protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER) + end - set_access_levels! + if protected_branch.merge_access_level.blank? + protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER) + end end - true + protected_branch rescue ActiveRecord::RecordInvalid - false + protected_branch end end end diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index 58f2f774bae..da4c96b3e5f 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -1,22 +1,13 @@ module ProtectedBranches - class UpdateService < ProtectedBranches::BaseService + class UpdateService < BaseService attr_reader :protected_branch - def initialize(project, current_user, id, params = {}) - super(project, current_user, params) - @protected_branch = ProtectedBranch.find(id) - end - - def execute + def execute(protected_branch) raise Gitlab::Access::AccessDeniedError unless current_user.can?(:admin_project, project) - ProtectedBranch.transaction do - set_access_levels! - end - - true - rescue ActiveRecord::RecordInvalid - false + @protected_branch = protected_branch + @protected_branch.update(params) + @protected_branch end end end diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index a6956c8e69f..2498b57afb4 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -26,4 +26,4 @@ = paginate @protected_branches, theme: 'gitlab' :javascript - new ProtectedBranchesAccessSelect($(".protected-branches-list"), true); + new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false); diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 2fc6081e448..498e412235e 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -18,12 +18,12 @@ = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level = dropdown_tag(protected_branch.merge_access_level.humanize, options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', - data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_merge" }}) + data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "merge_access_level" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level = dropdown_tag(protected_branch.push_access_level.humanize, options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', - data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "allowed_to_push" }}) + data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "push_access_level" }}) - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 75c2063027a..8270da6cd27 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -32,20 +32,20 @@ are supported. .form-group - = f.hidden_field :allowed_to_merge - = f.label :allowed_to_merge, "Allowed to merge: ", class: "label-light append-bottom-0" + = hidden_field_tag 'protected_branch[merge_access_level_attributes][access_level]' + = label_tag "Allowed to merge: ", nil, class: "label-light append-bottom-0" = dropdown_tag("", options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[allowed_to_merge]" }}) + data: { field_name: "protected_branch[merge_access_level_attributes][access_level]" }}) .form-group - = f.hidden_field :allowed_to_push - = f.label :allowed_to_push, "Allowed to push: ", class: "label-light append-bottom-0" + = hidden_field_tag 'protected_branch[push_access_level_attributes][access_level]' + = label_tag "Allowed to push: ", nil, class: "label-light append-bottom-0" = dropdown_tag("", options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[allowed_to_push]" }}) + data: { field_name: "protected_branch[push_access_level_attributes][access_level]" }}) = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true @@ -54,4 +54,4 @@ = render "branches_list" :javascript - new ProtectedBranchesAccessSelect($(".new_protected_branch"), false); + new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true); diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb index 3031574fe2a..5c14d449e71 100644 --- a/db/migrate/20160705054938_add_protected_branches_push_access.rb +++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb @@ -5,7 +5,9 @@ class AddProtectedBranchesPushAccess < ActiveRecord::Migration def change create_table :protected_branch_push_access_levels do |t| t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false - t.integer :access_level, default: 0, null: false + + # Gitlab::Access::MASTER == 40 + t.integer :access_level, default: 40, null: false t.timestamps null: false end diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb index cf1cdb8b3b6..789e3e04220 100644 --- a/db/migrate/20160705054952_add_protected_branches_merge_access.rb +++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb @@ -5,7 +5,9 @@ class AddProtectedBranchesMergeAccess < ActiveRecord::Migration def change create_table :protected_branch_merge_access_levels do |t| t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false - t.integer :access_level, default: 0, null: false + + # Gitlab::Access::MASTER == 40 + t.integer :access_level, default: 40, null: false t.timestamps null: false end diff --git a/db/schema.rb b/db/schema.rb index 7a5eded8e02..2d2ae5fd840 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -868,19 +868,19 @@ ActiveRecord::Schema.define(version: 20160722221922) do add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree create_table "protected_branch_merge_access_levels", force: :cascade do |t| - t.integer "protected_branch_id", null: false - t.integer "access_level", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 40, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree create_table "protected_branch_push_access_levels", force: :cascade do |t| - t.integer "protected_branch_id", null: false - t.integer "access_level", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 40, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 4133a1f7a6b..a77afe634f6 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -53,19 +53,37 @@ module API @branch = user_project.repository.find_branch(params[:branch]) not_found!('Branch') unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) + + developers_can_merge = to_boolean(params[:developers_can_merge]) + developers_can_push = to_boolean(params[:developers_can_push]) + protected_branch_params = { - name: @branch.name, - developers_can_push: params[:developers_can_push], - developers_can_merge: params[:developers_can_merge] + name: @branch.name } - service = if protected_branch - ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch.id, protected_branch_params) - else - ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params) - end + unless developers_can_merge.nil? + protected_branch_params.merge!({ + merge_access_level_attributes: { + access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + } + }) + end - service.execute + unless developers_can_push.nil? + protected_branch_params.merge!({ + push_access_level_attributes: { + access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + } + }) + end + + if protected_branch + service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params) + service.execute(protected_branch) + else + service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params) + service.execute + end present @branch, with: Entities::RepoBranch, project: user_project end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e51bee5c846..4eb95d8a215 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -127,12 +127,12 @@ module API expose :developers_can_push do |repo_branch, options| project = options[:project] - project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.developers? } + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.access_level == Gitlab::Access::DEVELOPER } end expose :developers_can_merge do |repo_branch, options| project = options[:project] - project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.developers? } + project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.access_level == Gitlab::Access::DEVELOPER } end end diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb index 24a9b78f0c2..5575852c2d7 100644 --- a/spec/factories/protected_branches.rb +++ b/spec/factories/protected_branches.rb @@ -4,20 +4,26 @@ FactoryGirl.define do project after(:create) do |protected_branch| - protected_branch.create_push_access_level!(access_level: :masters) - protected_branch.create_merge_access_level!(access_level: :masters) + protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER) + protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER) end trait :developers_can_push do - after(:create) { |protected_branch| protected_branch.push_access_level.developers! } + after(:create) do |protected_branch| + protected_branch.push_access_level.update!(access_level: Gitlab::Access::DEVELOPER) + end end trait :developers_can_merge do - after(:create) { |protected_branch| protected_branch.merge_access_level.developers! } + after(:create) do |protected_branch| + protected_branch.merge_access_level.update!(access_level: Gitlab::Access::DEVELOPER) + end end trait :no_one_can_push do - after(:create) { |protected_branch| protected_branch.push_access_level.no_one! } + after(:create) do |protected_branch| + protected_branch.push_access_level.update!(access_level: Gitlab::Access::NO_ACCESS) + end end end end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index dac2bcf9efd..57734b33a44 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -91,12 +91,12 @@ feature 'Projected Branches', feature: true, js: true do set_protected_branch_name('master') within('.new_protected_branch') do find(".allowed-to-push").click - click_on access_type_name + within(".dropdown.open .dropdown-menu") { click_on access_type_name } end click_on "Protect" expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.allowed_to_push).to eq(access_type_id) + expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id) end it "allows updating protected branches so that #{access_type_name} can push to them" do @@ -112,7 +112,7 @@ feature 'Projected Branches', feature: true, js: true do end wait_for_ajax - expect(ProtectedBranch.last.allowed_to_push).to eq(access_type_id) + expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id) end end @@ -122,12 +122,12 @@ feature 'Projected Branches', feature: true, js: true do set_protected_branch_name('master') within('.new_protected_branch') do find(".allowed-to-merge").click - click_on access_type_name + within(".dropdown.open .dropdown-menu") { click_on access_type_name } end click_on "Protect" expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.allowed_to_merge).to eq(access_type_id) + expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id) end it "allows updating protected branches so that #{access_type_name} can merge to them" do @@ -143,7 +143,7 @@ feature 'Projected Branches', feature: true, js: true do end wait_for_ajax - expect(ProtectedBranch.last.allowed_to_merge).to eq(access_type_id) + expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id) end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 621eced83f6..ffa998dffc3 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -227,8 +227,8 @@ describe GitPushService, services: true do expect(project.default_branch).to eq("master") execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) expect(project.protected_branches).not_to be_empty - expect(project.protected_branches.first.allowed_to_push).to eq('masters') - expect(project.protected_branches.first.allowed_to_merge).to eq('masters') + expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER) + expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::MASTER) end it "when pushing a branch for the first time with default branch protection disabled" do @@ -249,8 +249,8 @@ describe GitPushService, services: true do execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) expect(project.protected_branches).not_to be_empty - expect(project.protected_branches.last.allowed_to_push).to eq('developers') - expect(project.protected_branches.last.allowed_to_merge).to eq('masters') + expect(project.protected_branches.last.push_access_level.access_level).to eq(Gitlab::Access::DEVELOPER) + expect(project.protected_branches.last.merge_access_level.access_level).to eq(Gitlab::Access::MASTER) end it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do @@ -260,8 +260,8 @@ describe GitPushService, services: true do expect(project.default_branch).to eq("master") execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) expect(project.protected_branches).not_to be_empty - expect(project.protected_branches.first.allowed_to_push).to eq('masters') - expect(project.protected_branches.first.allowed_to_merge).to eq('developers') + expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER) + expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::DEVELOPER) end it "when pushing new commits to existing branch" do From cebcc417eda08711ad17a433d6d9b4f49830c04c Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 29 Jul 2016 12:31:15 +0530 Subject: [PATCH 115/198] Implement final review comments from @rymai. 1. Instantiate `ProtectedBranchesAccessSelect` from `dispatcher` 2. Use `can?(user, ...)` instead of `user.can?(...)` 3. Add `DOWNTIME` notes to all migrations added in !5081. 4. Add an explicit `down` method for migrations removing the `developers_can_push` and `developers_can_merge` columns, ensuring that the columns created (on rollback) have the appropriate defaults. 5. Remove duplicate CHANGELOG entries. 6. Blank lines after guard clauses. --- CHANGELOG | 1 - app/assets/javascripts/dispatcher.js | 5 +++++ app/models/protected_branch/merge_access_level.rb | 1 + app/models/protected_branch/push_access_level.rb | 1 + app/services/protected_branches/create_service.rb | 2 +- app/services/protected_branches/update_service.rb | 2 +- .../protected_branches/_branches_list.html.haml | 3 --- .../projects/protected_branches/index.html.haml | 3 --- ...0705054938_add_protected_branches_push_access.rb | 2 ++ ...705054952_add_protected_branches_merge_access.rb | 2 ++ ..._can_merge_to_protected_branches_merge_access.rb | 9 +++++++++ ...rs_can_push_to_protected_branches_push_access.rb | 9 +++++++++ ...e_developers_can_push_from_protected_branches.rb | 13 ++++++++++++- ..._developers_can_merge_from_protected_branches.rb | 13 ++++++++++++- 14 files changed, 55 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4f1da451df0..2b04c15b827 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -127,7 +127,6 @@ v 8.10.0 - Allow to define manual actions/builds on Pipelines and Environments - Fix pagination when sorting by columns with lots of ties (like priority) - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times. !5020 - - Add "No one can push" as an option for protected branches. !5081 - Updated project header design - Issuable collapsed assignee tooltip is now the users name - Fix compare view not changing code view rendering style diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d212d66da1b..9e6901962c6 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -171,6 +171,11 @@ break; case 'search:show': new Search(); + break; + case 'projects:protected_branches:index': + new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true); + new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false); + break; } switch (path.first()) { case 'admin': diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 25a6ca6a8ee..b1112ee737d 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -14,6 +14,7 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base def check_access(user) return true if user.is_admin? + project.team.max_member_access(user.id) >= access_level end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 1999316aa26..6a5e49cf453 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -17,6 +17,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base def check_access(user) return false if access_level == Gitlab::Access::NO_ACCESS return true if user.is_admin? + project.team.max_member_access(user.id) >= access_level end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index 50f79f491ce..6150a2a83c9 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -3,7 +3,7 @@ module ProtectedBranches attr_reader :protected_branch def execute - raise Gitlab::Access::AccessDeniedError unless current_user.can?(:admin_project, project) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) protected_branch = project.protected_branches.new(params) diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index da4c96b3e5f..89d8ba60134 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -3,7 +3,7 @@ module ProtectedBranches attr_reader :protected_branch def execute(protected_branch) - raise Gitlab::Access::AccessDeniedError unless current_user.can?(:admin_project, project) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) @protected_branch = protected_branch @protected_branch.update(params) diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 2498b57afb4..0603a014008 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -24,6 +24,3 @@ = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } = paginate @protected_branches, theme: 'gitlab' - -:javascript - new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false); diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 8270da6cd27..4efe44c7233 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -52,6 +52,3 @@ %hr = render "branches_list" - -:javascript - new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true); diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb index 5c14d449e71..f27295524e1 100644 --- a/db/migrate/20160705054938_add_protected_branches_push_access.rb +++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb @@ -2,6 +2,8 @@ # for more information on how to write migrations for GitLab. class AddProtectedBranchesPushAccess < ActiveRecord::Migration + DOWNTIME = false + def change create_table :protected_branch_push_access_levels do |t| t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb index 789e3e04220..32adfa266cd 100644 --- a/db/migrate/20160705054952_add_protected_branches_merge_access.rb +++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb @@ -2,6 +2,8 @@ # for more information on how to write migrations for GitLab. class AddProtectedBranchesMergeAccess < ActiveRecord::Migration + DOWNTIME = false + def change create_table :protected_branch_merge_access_levels do |t| t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false diff --git a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb index c2b278ce673..fa93936ced7 100644 --- a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb +++ b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb @@ -2,6 +2,15 @@ # for more information on how to write migrations for GitLab. class MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::Migration + DOWNTIME = true + DOWNTIME_REASON = <<-HEREDOC + We're creating a `merge_access_level` for each `protected_branch`. If a user creates a `protected_branch` while this + is running, we might be left with a `protected_branch` _without_ an associated `merge_access_level`. The `protected_branches` + table must not change while this is running, so downtime is required. + + https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081#note_13247410 + HEREDOC + def up execute <<-HEREDOC INSERT into protected_branch_merge_access_levels (protected_branch_id, access_level, created_at, updated_at) diff --git a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb index 5bc70283f60..56f6159d1d8 100644 --- a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb +++ b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb @@ -2,6 +2,15 @@ # for more information on how to write migrations for GitLab. class MoveFromDevelopersCanPushToProtectedBranchesPushAccess < ActiveRecord::Migration + DOWNTIME = true + DOWNTIME_REASON = <<-HEREDOC + We're creating a `push_access_level` for each `protected_branch`. If a user creates a `protected_branch` while this + is running, we might be left with a `protected_branch` _without_ an associated `push_access_level`. The `protected_branches` + table must not change while this is running, so downtime is required. + + https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081#note_13247410 + HEREDOC + def up execute <<-HEREDOC INSERT into protected_branch_push_access_levels (protected_branch_id, access_level, created_at, updated_at) diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb index ad6ad43686d..f563f660ddf 100644 --- a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb +++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb @@ -2,7 +2,18 @@ # for more information on how to write migrations for GitLab. class RemoveDevelopersCanPushFromProtectedBranches < ActiveRecord::Migration - def change + include Gitlab::Database::MigrationHelpers + + # This is only required for `#down` + disable_ddl_transaction! + + DOWNTIME = false + + def up remove_column :protected_branches, :developers_can_push, :boolean end + + def down + add_column_with_default(:protected_branches, :developers_can_push, :boolean, default: false, null: false) + end end diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb index 084914e423a..aa71e06d36e 100644 --- a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb +++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb @@ -2,7 +2,18 @@ # for more information on how to write migrations for GitLab. class RemoveDevelopersCanMergeFromProtectedBranches < ActiveRecord::Migration - def change + include Gitlab::Database::MigrationHelpers + + # This is only required for `#down` + disable_ddl_transaction! + + DOWNTIME = false + + def up remove_column :protected_branches, :developers_can_merge, :boolean end + + def down + add_column_with_default(:protected_branches, :developers_can_merge, :boolean, default: false, null: false) + end end From 002ad215818450d2cbbc5fa065850a953dc7ada8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 20 Jul 2016 20:13:02 +0200 Subject: [PATCH 116/198] Method for returning issues readable by a user The method Ability.issues_readable_by_user takes a list of users and an optional user and returns an Array of issues readable by said user. This method in turn is used by Banzai::ReferenceParser::IssueParser#nodes_visible_to_user so this method no longer needs to get all the available abilities just to check if a user has the "read_issue" ability. To test this I benchmarked an issue with 222 comments on my development environment. Using these changes the time spent in nodes_visible_to_user was reduced from around 120 ms to around 40 ms. --- CHANGELOG | 1 + app/models/ability.rb | 10 + app/models/issue.rb | 28 ++ lib/banzai/reference_parser/issue_parser.rb | 7 +- .../reference_parser/issue_parser_spec.rb | 12 +- spec/models/ability_spec.rb | 48 ++++ spec/models/concerns/mentionable_spec.rb | 6 +- spec/models/issue_spec.rb | 253 ++++++++++++++++++ 8 files changed, 355 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d555d23860d..e18a133cfb0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.11.0 (unreleased) - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects - Retrieve rendered HTML from cache in one request - Fix renaming repository when name contains invalid chararacters under project settings + - Optimize checking if a user has read access to a list of issues !5370 - Nokogiri's various parsing methods are now instrumented - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Add build event color in HipChat messages (David Eisner) diff --git a/app/models/ability.rb b/app/models/ability.rb index e47c5539f60..d95a2507199 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -47,6 +47,16 @@ class Ability end end + # Returns an Array of Issues that can be read by the given user. + # + # issues - The issues to reduce down to those readable by the user. + # user - The User for which to check the issues + def issues_readable_by_user(issues, user = nil) + return issues if user && user.admin? + + issues.select { |issue| issue.visible_to_user?(user) } + end + # List of possible abilities for anonymous user def anonymous_abilities(user, subject) if subject.is_a?(PersonalSnippet) diff --git a/app/models/issue.rb b/app/models/issue.rb index d9428ebc9fb..11f734cfc6d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -230,6 +230,34 @@ class Issue < ActiveRecord::Base self.closed_by_merge_requests(current_user).empty? end + # Returns `true` if the current issue can be viewed by either a logged in User + # or an anonymous user. + def visible_to_user?(user = nil) + user ? readable_by?(user) : publicly_visible? + end + + # Returns `true` if the given User can read the current Issue. + def readable_by?(user) + if user.admin? + true + elsif project.owner == user + true + elsif confidential? + author == user || + assignee == user || + project.team.member?(user, Gitlab::Access::REPORTER) + else + project.public? || + project.internal? && !user.external? || + project.team.member?(user) + end + end + + # Returns `true` if this Issue is visible to everybody. + def publicly_visible? + project.public? && !confidential? + end + def overdue? due_date.try(:past?) || false end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index f306079d833..6c20dec5734 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -9,10 +9,11 @@ module Banzai issues = issues_for_nodes(nodes) - nodes.select do |node| - issue = issue_for_node(issues, node) + readable_issues = Ability. + issues_readable_by_user(issues.values, user).to_set - issue ? can?(user, :read_issue, issue) : false + nodes.select do |node| + readable_issues.include?(issue_for_node(issues, node)) end end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 514c752546d..85cfe728b6a 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -16,17 +16,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do end it 'returns the nodes when the user can read the issue' do - expect(Ability.abilities).to receive(:allowed?). - with(user, :read_issue, issue). - and_return(true) + expect(Ability).to receive(:issues_readable_by_user). + with([issue], user). + and_return([issue]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end it 'returns an empty Array when the user can not read the issue' do - expect(Ability.abilities).to receive(:allowed?). - with(user, :read_issue, issue). - and_return(false) + expect(Ability).to receive(:issues_readable_by_user). + with([issue], user). + and_return([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index cd5f40fe3d2..853f6943cef 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -170,4 +170,52 @@ describe Ability, lib: true do end end end + + describe '.issues_readable_by_user' do + context 'with an admin user' do + it 'returns all given issues' do + user = build(:user, admin: true) + issue = build(:issue) + + expect(described_class.issues_readable_by_user([issue], user)). + to eq([issue]) + end + end + + context 'with a regular user' do + it 'returns the issues readable by the user' do + user = build(:user) + issue = build(:issue) + + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(described_class.issues_readable_by_user([issue], user)). + to eq([issue]) + end + + it 'returns an empty Array when no issues are readable' do + user = build(:user) + issue = build(:issue) + + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(described_class.issues_readable_by_user([issue], user)).to eq([]) + end + end + + context 'without a regular user' do + it 'returns issues that are publicly visible' do + hidden_issue = build(:issue) + visible_issue = build(:issue) + + expect(hidden_issue).to receive(:publicly_visible?).and_return(false) + expect(visible_issue).to receive(:publicly_visible?).and_return(true) + + issues = described_class. + issues_readable_by_user([hidden_issue, visible_issue]) + + expect(issues).to eq([visible_issue]) + end + end + end end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 5e652660e2c..549b0042038 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -68,7 +68,7 @@ describe Issue, "Mentionable" do describe '#create_cross_references!' do let(:project) { create(:project) } - let(:author) { double('author') } + let(:author) { build(:user) } let(:commit) { project.commit } let(:commit2) { project.commit } @@ -88,6 +88,10 @@ describe Issue, "Mentionable" do let(:author) { create(:author) } let(:issues) { create_list(:issue, 2, project: project, author: author) } + before do + project.team << [author, Gitlab::Access::DEVELOPER] + end + context 'before changes are persisted' do it 'ignores pre-existing references' do issue = create_issue(description: issues[0].to_reference) diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 6a897c96690..3259f795296 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -306,4 +306,257 @@ describe Issue, models: true do expect(user2.assigned_open_issues_count).to eq(1) end end + + describe '#visible_to_user?' do + context 'with a user' do + let(:user) { build(:user) } + let(:issue) { build(:issue) } + + it 'returns true when the issue is readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(issue.visible_to_user?(user)).to eq(true) + end + + it 'returns false when the issue is not readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(issue.visible_to_user?(user)).to eq(false) + end + end + + context 'without a user' do + let(:issue) { build(:issue) } + + it 'returns true when the issue is publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(true) + + expect(issue.visible_to_user?).to eq(true) + end + + it 'returns false when the issue is not publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(false) + + expect(issue.visible_to_user?).to eq(false) + end + end + end + + describe '#readable_by?' do + describe 'with a regular user that is not a team member' do + let(:user) { create(:user) } + + context 'using a public project' do + let(:project) { create(:empty_project, :public) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, project: project, confidential: true) + + expect(issue).not_to be_readable_by(user) + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + context 'using an internal user' do + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + + context 'using an external user' do + before do + allow(user).to receive(:external?).and_return(true) + end + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + + context 'when the user is the project owner' do + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + end + end + + context 'with a regular user that is a team member' do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + context 'using a public project' do + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + end + + context 'with an admin user' do + let(:project) { create(:empty_project) } + let(:user) { create(:user, admin: true) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + end + + describe '#publicly_visible?' do + context 'using a public project' do + let(:project) { create(:empty_project, :public) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + end end From 6595d6aab078497293d657cd05d0af61c6cbda0b Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Thu, 28 Jul 2016 14:44:00 +0200 Subject: [PATCH 117/198] Bump gitlab_git to speedup DiffCollection iterations --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- app/views/projects/diffs/_stats.html.haml | 2 +- app/views/projects/diffs/_warning.html.haml | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2b04c15b827..5e181e865a6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.11.0 (unreleased) - The overhead of instrumented method calls has been reduced - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` + - Bump gitlab_git to speedup DiffCollection iterations - Make branches sortable without push permission !5462 (winniehell) - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem diff --git a/Gemfile b/Gemfile index a7d5e0e3e89..5f247abd2fc 100644 --- a/Gemfile +++ b/Gemfile @@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.4.1' +gem 'gitlab_git', '~> 10.4.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 150a98bb7d0..7b4175ea824 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.4.1) + gitlab_git (10.4.2) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -870,7 +870,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.4.1) + gitlab_git (~> 10.4.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index ea2a3e01277..e751dabdf99 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -2,7 +2,7 @@ .commit-stat-summary Showing = link_to '#', class: 'js-toggle-button' do - %strong #{pluralize(diff_files.count, "changed file")} + %strong #{pluralize(diff_files.size, "changed file")} with %strong.cgreen #{diff_files.sum(&:added_lines)} additions and diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 10fa1ddf2e5..295a1b62535 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -11,5 +11,5 @@ = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm" %p To preserve performance only - %strong #{diff_files.count} of #{diff_files.real_size} + %strong #{diff_files.size} of #{diff_files.real_size} files are displayed. From be9aa7f19474424991923f128053e2523fa166d8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 26 Jul 2016 09:35:47 +0200 Subject: [PATCH 118/198] Add an URL field to Environments This MR adds a string (thus max 255 chars) field to the enviroments table to expose it later in other features. --- CHANGELOG | 1 + .../projects/environments_controller.rb | 23 ++++++--- app/models/environment.rb | 4 ++ .../projects/environments/_form.html.haml | 29 ++++++++--- .../projects/environments/edit.html.haml | 6 +++ app/views/projects/environments/new.html.haml | 14 ++---- .../projects/environments/show.html.haml | 2 +- config/routes.rb | 2 +- ...5083350_add_external_url_to_enviroments.rb | 12 +++++ db/schema.rb | 3 +- .../projects/environments_controller_spec.rb | 50 +++++++++++++++++++ spec/factories/environments.rb | 1 + spec/features/environments_spec.rb | 4 +- spec/models/environment_spec.rb | 10 ++++ 14 files changed, 133 insertions(+), 28 deletions(-) create mode 100644 app/views/projects/environments/edit.html.haml create mode 100644 db/migrate/20160725083350_add_external_url_to_enviroments.rb create mode 100644 spec/controllers/projects/environments_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 2b04c15b827..0292df068fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 8.11.0 (unreleased) - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes - Add "No one can push" as an option for protected branches. !5081 + - Environments have an url to link to - Limit git rev-list output count to one in forced push check - Clean up unused routes (Josef Strzibny) - Add green outline to New Branch button. !5447 (winniehell) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 4b433796161..1f5c7506212 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,8 +2,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:destroy] - before_action :environment, only: [:show, :destroy] + before_action :authorize_update_environment!, only: [:edit, :destroy] + before_action :environment, only: [:show, :edit, :update, :destroy] def index @environments = project.environments @@ -17,13 +17,24 @@ class Projects::EnvironmentsController < Projects::ApplicationController @environment = project.environments.new end + def edit + end + def create - @environment = project.environments.create(create_params) + @environment = project.environments.create(environment_params) if @environment.persisted? redirect_to namespace_project_environment_path(project.namespace, project, @environment) else - render 'new' + render :new + end + end + + def update + if @environment.update(environment_params) + redirect_to namespace_project_environment_path(project.namespace, project, @environment) + else + render :edit end end @@ -39,8 +50,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController private - def create_params - params.require(:environment).permit(:name) + def environment_params + params.require(:environment).permit(:name, :external_url) end def environment diff --git a/app/models/environment.rb b/app/models/environment.rb index ac3a571a1f3..9eff0fdab03 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -10,6 +10,10 @@ class Environment < ActiveRecord::Base format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + validates :external_url, + uniqueness: { scope: :project_id }, + length: { maximum: 255 } + def last_deployment deployments.last end diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml index c07f4bd510c..6d040f5cfe6 100644 --- a/app/views/projects/environments/_form.html.haml +++ b/app/views/projects/environments/_form.html.haml @@ -1,7 +1,22 @@ -= form_for @environment, url: namespace_project_environments_path(@project.namespace, @project), html: { class: 'col-lg-9' } do |f| - = form_errors(@environment) - .form-group - = f.label :name, 'Name', class: 'label-light' - = f.text_field :name, required: true, class: 'form-control' - = f.submit 'Create environment', class: 'btn btn-create' - = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel' +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Environments + %p + Environments allow you to track deployments of your application + = succeed "." do + = link_to "Read more about environments", help_page_path("ci/environments") + + = form_for [@project.namespace.becomes(Namespace), @project, @environment], html: { class: 'col-lg-9' } do |f| + = form_errors(@environment) + + .form-group + = f.label :name, 'Name', class: 'label-light' + = f.text_field :name, required: true, class: 'form-control' + .form-group + = f.label :external_url, 'External URL', class: 'label-light' + = f.url_field :external_url, class: 'form-control' + + .form-actions + = f.submit 'Save', class: 'btn btn-save' + = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml new file mode 100644 index 00000000000..6d1bdb9320f --- /dev/null +++ b/app/views/projects/environments/edit.html.haml @@ -0,0 +1,6 @@ +- page_title "Edit", @environment.name, "Environments" + +%h3.page-title + Edit environment +%hr += render 'form' diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index 89e06567196..e51667ade2d 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -1,12 +1,6 @@ - page_title 'New Environment' -.row.prepend-top-default.append-bottom-default - .col-lg-3 - %h4.prepend-top-0 - New Environment - %p - Environments allow you to track deployments of your application - = succeed "." do - = link_to "Read more about environments", help_page_path("ci/environments") - - = render 'form' +%h3.page-title + New environment +%hr += render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index b8b1ce52a91..a07436ad7c9 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -6,10 +6,10 @@ .top-area .col-md-9 %h3.page-title= @environment.name.capitalize - .col-md-3 .nav-controls - if can?(current_user, :update_environment, @environment) + = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete - if @deployments.blank? diff --git a/config/routes.rb b/config/routes.rb index 308d83af57e..ced204be7f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -741,7 +741,7 @@ Rails.application.routes.draw do end end - resources :environments, only: [:index, :show, :new, :create, :destroy] + resources :environments, constraints: { id: /\d+/ } resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do diff --git a/db/migrate/20160725083350_add_external_url_to_enviroments.rb b/db/migrate/20160725083350_add_external_url_to_enviroments.rb new file mode 100644 index 00000000000..e887341159b --- /dev/null +++ b/db/migrate/20160725083350_add_external_url_to_enviroments.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddExternalUrlToEnviroments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:environments, :external_url, :string) + end +end diff --git a/db/schema.rb b/db/schema.rb index 2d2ae5fd840..4365af98962 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -427,9 +427,10 @@ ActiveRecord::Schema.define(version: 20160722221922) do create_table "environments", force: :cascade do |t| t.integer "project_id" - t.string "name", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" + t.string "external_url" end add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb new file mode 100644 index 00000000000..b91a99d6b2e --- /dev/null +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Projects::EnvironmentsController do + let(:environment) { create(:environment) } + let(:project) { environment.project } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + + sign_in(user) + end + + render_views + + describe 'GET show' do + context 'with valid id' do + it 'responds with a status code 200' do + get :show, namespace_id: project.namespace, project_id: project, id: environment.id + + expect(response).to be_ok + end + end + + context 'with invalid id' do + it 'responds with a status code 404' do + get :show, namespace_id: project.namespace, project_id: project, id: 12345 + + expect(response).to be_not_found + end + end + end + + describe 'GET edit' do + it 'responds with a status code 200' do + get :edit, namespace_id: project.namespace, project_id: project, id: environment.id + + expect(response).to be_ok + end + end + + describe 'PATCH #update' do + it 'responds with a 302' do + patch :update, namespace_id: project.namespace, project_id: + project, id: environment.id, environment: { external_url: 'https://git.gitlab.com' } + + expect(response).to have_http_status(302) + end + end +end diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index 07265c26ca3..846cccfc7fa 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -3,5 +3,6 @@ FactoryGirl.define do sequence(:name) { |n| "environment#{n}" } project factory: :empty_project + sequence(:external_url) { |n| "https://env#{n}.example.gitlab.com" } end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index a7d9f2a0c72..fcd41b38413 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -140,7 +140,7 @@ feature 'Environments', feature: true do context 'for valid name' do before do fill_in('Name', with: 'production') - click_on 'Create environment' + click_on 'Save' end scenario 'does create a new pipeline' do @@ -151,7 +151,7 @@ feature 'Environments', feature: true do context 'for invalid name' do before do fill_in('Name', with: 'name with spaces') - click_on 'Create environment' + click_on 'Save' end scenario 'does show errors' do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 7629af6a570..6c11cfc4c9b 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -11,4 +11,14 @@ describe Environment, models: true do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_within(0..255) } + + it { is_expected.to validate_length_of(:external_url).is_within(0..255) } + + # To circumvent a not null violation of the name column: + # https://github.com/thoughtbot/shoulda-matchers/issues/336 + it 'validates uniqueness of :external_url' do + create(:environment) + + is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) + end end From 84cd2120952e7ee4095cb4b5d7c959f2c11610c5 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 26 Jul 2016 09:37:02 +0200 Subject: [PATCH 119/198] Add API support for environments --- doc/api/enviroments.md | 118 +++++++++++++++++++++++++ lib/api/api.rb | 1 + lib/api/entities.rb | 4 + lib/api/environments.rb | 87 ++++++++++++++++++ spec/requests/api/environments_spec.rb | 103 +++++++++++++++++++++ 5 files changed, 313 insertions(+) create mode 100644 doc/api/enviroments.md create mode 100644 lib/api/environments.rb create mode 100644 spec/requests/api/environments_spec.rb diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md new file mode 100644 index 00000000000..c4b844fe77e --- /dev/null +++ b/doc/api/enviroments.md @@ -0,0 +1,118 @@ +# Environments + +## List environments + +Get all environments for a given project. + +``` +GET /projects/:id/environments +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "Env1", + "external_url": "https://env1.example.gitlab.com" + } +] +``` + +## Create a new environment + +Creates a new environment with the given name and external_url. + +It returns 200 if the environment was successfully created, 400 for wrong parameters +and 409 if the environment already exists. + +``` +POST /projects/:id/environment +``` + +| Attribute | Type | Required | Description | +| ------------- | ------- | -------- | ---------------------------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the environment | +| `external_url` | string | yes | Place to link to for this environment | + +```bash +curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments" +``` + +Example response: + +```json +{ + "id": 1, + "name": "deploy", + "external_url": "https://deploy.example.gitlab.com" +} +``` + +## Delete an environment + +It returns 200 if the environment was successfully deleted, and 404 if the environment does not exist. + +``` +DELETE /projects/:id/environments/:environment_id +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the project | +| `environment_id` | integer | yes | The ID of the environment | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "deploy", + "external_url": "https://deploy.example.gitlab.com" +} +``` + +## Edit an existing environment + +Updates an existing environments name and/or external_url. + +It returns 200 if the label was successfully updated, In case of an error, an additional error message is returned. + +``` +PUT /projects/:id/environments/:environments_id +``` + +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer | yes | The ID of the project | +| `environment_id` | integer | yes | The ID of the environment | The ID of the environment | +| `name` | string | no | The new name of the environment | +| `external_url` | string | no | The new external_url | + +```bash +curl -X PUT --data "name=staging&external_url=https://staging.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "staging", + "external_url": "https://staging.example.gitlab.com" +} +``` diff --git a/lib/api/api.rb b/lib/api/api.rb index 3d7d67510a8..9c960d74495 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -32,6 +32,7 @@ module API mount ::API::CommitStatuses mount ::API::Commits mount ::API::DeployKeys + mount ::API::Environments mount ::API::Files mount ::API::GroupMembers mount ::API::Groups diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4eb95d8a215..3e21b7a0b8a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -496,6 +496,10 @@ module API expose :key, :value end + class Environment < Grape::Entity + expose :id, :name, :external_url + end + class RepoLicense < Grape::Entity expose :key, :name, :nickname expose :featured, as: :popular diff --git a/lib/api/environments.rb b/lib/api/environments.rb new file mode 100644 index 00000000000..66a047f72fc --- /dev/null +++ b/lib/api/environments.rb @@ -0,0 +1,87 @@ +module API + # Environments RESTfull API endpoints + class Environments < Grape::API + before { authenticate! } + + resource :projects do + # Get all labels of the project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/environments + get ':id/environments' do + authorize! :read_environment, user_project + + present paginate(user_project.environments), with: Entities::Environment + end + + # Creates a new environment + # + # Parameters: + # id (required) - The ID of a project + # name (required) - The name of the environment to be created + # external_url (optional) - URL on which this deployment is viewable + # + # Example Request: + # POST /projects/:id/labels + post ':id/environments' do + authorize! :create_environment, user_project + required_attributes! [:name] + + attrs = attributes_for_keys [:name, :external_url] + environment = user_project.environments.find_by(name: attrs[:name]) + + conflict!('Environment already exists') if environment + + environment = user_project.environments.create(attrs) + + if environment.valid? + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + + # Deletes an existing environment + # + # Parameters: + # id (required) - The ID of a project + # environment_id (required) - The name of the environment to be deleted + # + # Example Request: + # DELETE /projects/:id/environments/:environment_id + delete ':id/environments/:environment_id' do + authorize! :admin_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + present environment.destroy, with: Entities::Environment + end + + # Updates an existing environment + # + # Parameters: + # id (required) - The ID of a project + # environment_id (required) - The ID of the environment + # name (optional) - The name of the label to be deleted + # external_url (optional) - The new name of the label + # + # Example Request: + # PUT /projects/:id/environments/:environment_id + put ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + attrs = attributes_for_keys [:name, :external_url] + + if environment.update(attrs) + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + end + end +end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb new file mode 100644 index 00000000000..822139dbf3b --- /dev/null +++ b/spec/requests/api/environments_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } + let!(:environment) { create(:environment, project: project) } + + before do + project.team << [user, :master] + end + + describe 'GET /projects/:id/environments' do + context 'as member of the project' do + it 'should return project labels' do + get api("/projects/#{project.id}/environments", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['name']).to eq(environment.name) + expect(json_response.first['external_url']).to eq(environment.external_url) + end + end + + context 'as non member' do + it 'should return a 404 status code' do + get api("/projects/#{project.id}/environments", non_member) + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /projects/:id/labels' do + context 'as a member' do + it 'creates a environment with valid params' do + post api("/projects/#{project.id}/environments", user), name: "mepmep" + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('mepmep') + expect(json_response['external']).to be nil + end + + it 'requires name to be passed' do + post api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com' + + expect(response).to have_http_status(400) + end + + it 'should return 409 if environment already exists' do + post api("/projects/#{project.id}/environments", user), name: environment.name + + expect(response).to have_http_status(409) + expect(json_response['message']).to eq('Environment already exists') + end + end + + context 'a non member' do + it 'rejects the request' do + post api("/projects/#{project.id}/environments", non_member) + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE /projects/:id/environments/:environment_id' do + context 'as a master' do + it 'should return 200 for an existing environment' do + delete api("/projects/#{project.id}/environments/#{environment.id}", user) + + expect(response).to have_http_status(200) + end + + it 'should return 404 for non existing id' do + delete api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + end + + describe 'PUT /projects/:id/environments/:environment_id' do + it 'should return 200 if name and external_url are changed' do + put api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep', external_url: 'mepmep.whatever.ninja' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq('mepmep.whatever.ninja') + end + + it 'should return 404 if the environment does not exist' do + put api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + end + end +end From 76e9b68439510af5c783a81b93944f1c8d96d150 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 26 Jul 2016 14:19:37 +0200 Subject: [PATCH 120/198] Incorporate feedback --- app/models/environment.rb | 10 +++++++- ...5083350_add_external_url_to_enviroments.rb | 3 --- db/schema.rb | 2 +- doc/api/enviroments.md | 7 +++--- lib/api/environments.rb | 5 +--- .../projects/environments_controller_spec.rb | 24 ++++++++++++------- spec/models/environment_spec.rb | 8 +++++++ spec/requests/api/environments_spec.rb | 15 ++++++------ 8 files changed, 45 insertions(+), 29 deletions(-) diff --git a/app/models/environment.rb b/app/models/environment.rb index 9eff0fdab03..baed106e8c8 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -3,6 +3,8 @@ class Environment < ActiveRecord::Base has_many :deployments + before_validation :nullify_external_url + validates :name, presence: true, uniqueness: { scope: :project_id }, @@ -12,9 +14,15 @@ class Environment < ActiveRecord::Base validates :external_url, uniqueness: { scope: :project_id }, - length: { maximum: 255 } + length: { maximum: 255 }, + allow_nil: true, + addressable_url: true def last_deployment deployments.last end + + def nullify_external_url + self.external_url = nil if self.external_url.blank? + end end diff --git a/db/migrate/20160725083350_add_external_url_to_enviroments.rb b/db/migrate/20160725083350_add_external_url_to_enviroments.rb index e887341159b..21a8abd310b 100644 --- a/db/migrate/20160725083350_add_external_url_to_enviroments.rb +++ b/db/migrate/20160725083350_add_external_url_to_enviroments.rb @@ -1,6 +1,3 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - class AddExternalUrlToEnviroments < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/schema.rb b/db/schema.rb index 4365af98962..5b35a528e71 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: 20160722221922) do +ActiveRecord::Schema.define(version: 20160726093600) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index c4b844fe77e..16bf2627fef 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -32,8 +32,7 @@ Example response: Creates a new environment with the given name and external_url. -It returns 200 if the environment was successfully created, 400 for wrong parameters -and 409 if the environment already exists. +It returns 200 if the environment was successfully created, 400 for wrong parameters. ``` POST /projects/:id/environment @@ -43,7 +42,7 @@ POST /projects/:id/environment | ------------- | ------- | -------- | ---------------------------- | | `id` | integer | yes | The ID of the project | | `name` | string | yes | The name of the environment | -| `external_url` | string | yes | Place to link to for this environment | +| `external_url` | string | no | Place to link to for this environment | ```bash curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments" @@ -88,7 +87,7 @@ Example response: ## Edit an existing environment -Updates an existing environments name and/or external_url. +Updates an existing environment's name and/or external_url. It returns 200 if the label was successfully updated, In case of an error, an additional error message is returned. diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 66a047f72fc..532baec42c7 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -30,9 +30,6 @@ module API required_attributes! [:name] attrs = attributes_for_keys [:name, :external_url] - environment = user_project.environments.find_by(name: attrs[:name]) - - conflict!('Environment already exists') if environment environment = user_project.environments.create(attrs) @@ -52,7 +49,7 @@ module API # Example Request: # DELETE /projects/:id/environments/:environment_id delete ':id/environments/:environment_id' do - authorize! :admin_environment, user_project + authorize! :update_environment, user_project environment = user_project.environments.find(params[:environment_id]) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index b91a99d6b2e..768105cae95 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -11,12 +11,10 @@ describe Projects::EnvironmentsController do sign_in(user) end - render_views - describe 'GET show' do context 'with valid id' do it 'responds with a status code 200' do - get :show, namespace_id: project.namespace, project_id: project, id: environment.id + get :show, environment_params expect(response).to be_ok end @@ -24,16 +22,18 @@ describe Projects::EnvironmentsController do context 'with invalid id' do it 'responds with a status code 404' do - get :show, namespace_id: project.namespace, project_id: project, id: 12345 + params = environment_params + params[:id] = 12345 + get :show, params - expect(response).to be_not_found + expect(response).to have_http_status(404) end end end describe 'GET edit' do it 'responds with a status code 200' do - get :edit, namespace_id: project.namespace, project_id: project, id: environment.id + get :edit, environment_params expect(response).to be_ok end @@ -41,10 +41,18 @@ describe Projects::EnvironmentsController do describe 'PATCH #update' do it 'responds with a 302' do - patch :update, namespace_id: project.namespace, project_id: - project, id: environment.id, environment: { external_url: 'https://git.gitlab.com' } + patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' }) + patch :update, patch_params expect(response).to have_http_status(302) end end + + def environment_params + { + namespace_id: project.namespace, + project_id: project, + id: environment.id + } + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 6c11cfc4c9b..ef2148be1bd 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -21,4 +21,12 @@ describe Environment, models: true do is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) end + + describe '#nullify_external_url' do + it 'replaces a blank url with nil' do + env = build(:environment, external_url: "") + + expect(env.save).to be true + end + end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 822139dbf3b..b731c58a206 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -5,7 +5,7 @@ describe API::API, api: true do let(:user) { create(:user) } let(:non_member) { create(:user) } - let(:project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } + let(:project) { create(:project, :private, namespace: user.namespace) } let!(:environment) { create(:environment, project: project) } before do @@ -14,7 +14,7 @@ describe API::API, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do - it 'should return project labels' do + it 'should return project environments' do get api("/projects/#{project.id}/environments", user) expect(response).to have_http_status(200) @@ -34,7 +34,7 @@ describe API::API, api: true do end end - describe 'POST /projects/:id/labels' do + describe 'POST /projects/:id/environments' do context 'as a member' do it 'creates a environment with valid params' do post api("/projects/#{project.id}/environments", user), name: "mepmep" @@ -50,11 +50,10 @@ describe API::API, api: true do expect(response).to have_http_status(400) end - it 'should return 409 if environment already exists' do + it 'should return 400 if environment already exists' do post api("/projects/#{project.id}/environments", user), name: environment.name - expect(response).to have_http_status(409) - expect(json_response['message']).to eq('Environment already exists') + expect(response).to have_http_status(400) end end @@ -87,11 +86,11 @@ describe API::API, api: true do describe 'PUT /projects/:id/environments/:environment_id' do it 'should return 200 if name and external_url are changed' do put api("/projects/#{project.id}/environments/#{environment.id}", user), - name: 'Mepmep', external_url: 'mepmep.whatever.ninja' + name: 'Mepmep', external_url: 'https://mepmep.whatever.ninja' expect(response).to have_http_status(200) expect(json_response['name']).to eq('Mepmep') - expect(json_response['external_url']).to eq('mepmep.whatever.ninja') + expect(json_response['external_url']).to eq('https://mepmep.whatever.ninja') end it 'should return 404 if the environment does not exist' do From d05af7b7c6975ae66808ed6676a1b947c7abe244 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Thu, 28 Jul 2016 07:09:40 +0200 Subject: [PATCH 121/198] Check for Ci::Build artifacts at database level --- CHANGELOG | 1 + app/models/ci/build.rb | 1 + .../projects/ci/pipelines/_pipeline.html.haml | 2 +- spec/factories/ci/builds.rb | 16 ++++++++++++++++ spec/features/pipelines_spec.rb | 10 ++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5e181e865a6..88f37735c69 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ v 8.11.0 (unreleased) - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` - Bump gitlab_git to speedup DiffCollection iterations - Make branches sortable without push permission !5462 (winniehell) + - Check for Ci::Build artifacts at database level on pipeline partial - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index aac78d75f57..08f396210c9 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -13,6 +13,7 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) } + scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual) } diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 2f7d54f0bdd..558c35553da 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -57,7 +57,7 @@ %td.pipeline-actions .controls.hidden-xs.pull-right - - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } + - artifacts = pipeline.builds.latest.with_artifacts_not_expired - actions = pipeline.manual_actions - if artifacts.present? || actions.any? .btn-group.inline diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 5e19e403c6b..1b32d560b16 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -90,5 +90,21 @@ FactoryGirl.define do build.save! end end + + trait :artifacts_expired do + after(:create) do |build, _| + build.artifacts_file = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), + 'application/zip') + + build.artifacts_metadata = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), + 'application/x-gzip') + + build.artifacts_expire_at = 1.minute.ago + + build.save! + end + end end end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 7f861db1969..377a9aba60d 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -116,9 +116,19 @@ describe "Pipelines" do it { expect(page).to have_link(with_artifacts.name) } end + context 'with artifacts expired' do + let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).not_to have_selector('.build-artifacts') } + end + context 'without artifacts' do let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + before { visit namespace_project_pipelines_path(project.namespace, project) } + it { expect(page).not_to have_selector('.build-artifacts') } end end From a42cce1b966046c21ec48b18435d38e68a20f7fa Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 29 Jul 2016 12:30:38 +0200 Subject: [PATCH 122/198] Improve code, remove unused validator, improve names --- lib/gitlab/ci/config/node/cache.rb | 4 +++- lib/gitlab/ci/config/node/commands.rb | 12 +++++------- lib/gitlab/ci/config/node/configurable.rb | 7 +++---- lib/gitlab/ci/config/node/global.rb | 4 ++-- lib/gitlab/ci/config/node/job.rb | 4 ++-- lib/gitlab/ci/config/node/null.rb | 2 +- lib/gitlab/ci/config/node/{while.rb => trigger.rb} | 4 ++-- lib/gitlab/ci/config/node/undefined.rb | 6 +----- lib/gitlab/ci/config/node/validators.rb | 9 --------- spec/lib/gitlab/ci/config/node/artifacts_spec.rb | 2 +- .../config/node/{while_spec.rb => trigger_spec.rb} | 4 ++-- 11 files changed, 22 insertions(+), 36 deletions(-) rename lib/gitlab/ci/config/node/{while.rb => trigger.rb} (83%) rename spec/lib/gitlab/ci/config/node/{while_spec.rb => trigger_spec.rb} (90%) diff --git a/lib/gitlab/ci/config/node/cache.rb b/lib/gitlab/ci/config/node/cache.rb index 21d96b220b8..b4bda2841ac 100644 --- a/lib/gitlab/ci/config/node/cache.rb +++ b/lib/gitlab/ci/config/node/cache.rb @@ -8,8 +8,10 @@ module Gitlab class Cache < Entry include Configurable + ALLOWED_KEYS = %i[key untracked paths] + validations do - validates :config, allowed_keys: %i[key untracked paths] + validates :config, allowed_keys: ALLOWED_KEYS end node :key, Node::Key, diff --git a/lib/gitlab/ci/config/node/commands.rb b/lib/gitlab/ci/config/node/commands.rb index f7e6950001e..d7657ae314b 100644 --- a/lib/gitlab/ci/config/node/commands.rb +++ b/lib/gitlab/ci/config/node/commands.rb @@ -11,22 +11,20 @@ module Gitlab validations do include LegacyValidationHelpers - validate :string_or_array_of_strings - - def string_or_array_of_strings - unless config_valid? + validate do + unless string_or_array_of_strings?(config) errors.add(:config, 'should be a string or an array of strings') end end - def config_valid? - validate_string(config) || validate_array_of_strings(config) + def string_or_array_of_strings?(field) + validate_string(field) || validate_array_of_strings(field) end end def value - [@config].flatten + Array(@config) end end end diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 93a9a253322..aedc28fe1d0 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -56,10 +56,9 @@ module Gitlab end define_method("#{symbol}_value") do - if @entries[symbol] - return unless @entries[symbol].valid? - @entries[symbol].value - end + return unless @entries[symbol] && @entries[symbol].valid? + + @entries[symbol].value end alias_method symbol.to_sym, "#{symbol}_value".to_sym diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index b545b78a940..ccd539fb003 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -42,7 +42,7 @@ module Gitlab super compose_jobs! - compose_stages! + compose_deprecated_entries! end def compose_jobs! @@ -54,7 +54,7 @@ module Gitlab @entries[:jobs] = factory.create! end - def compose_stages! + def compose_deprecated_entries! ## # Deprecated `:types` key workaround - if types are defined and # stages are not defined we use types definition as stages. diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb index ace79d829f2..e84737acbb9 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -66,10 +66,10 @@ module Gitlab node :services, Services, description: 'Services that will be used to execute this job.' - node :only, While, + node :only, Trigger, description: 'Refs policy this job will be executed for.' - node :except, While, + node :except, Trigger, description: 'Refs policy this job will be executed for.' node :variables, Variables, diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb index 880d29f663d..88a5f53f13c 100644 --- a/lib/gitlab/ci/config/node/null.rb +++ b/lib/gitlab/ci/config/node/null.rb @@ -3,7 +3,7 @@ module Gitlab class Config module Node ## - # This class represents an undefined and unspecified node. + # This class represents an undefined node. # # Implements the Null Object pattern. # diff --git a/lib/gitlab/ci/config/node/while.rb b/lib/gitlab/ci/config/node/trigger.rb similarity index 83% rename from lib/gitlab/ci/config/node/while.rb rename to lib/gitlab/ci/config/node/trigger.rb index 84d4352624d..d8b31975088 100644 --- a/lib/gitlab/ci/config/node/while.rb +++ b/lib/gitlab/ci/config/node/trigger.rb @@ -3,9 +3,9 @@ module Gitlab class Config module Node ## - # Entry that represents a ref and trigger policy for the job. + # Entry that represents a trigger policy for the job. # - class While < Entry + class Trigger < Entry include Validatable validations do diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index 84dab61e7e9..45fef8c3ae5 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -3,16 +3,12 @@ module Gitlab class Config module Node ## - # This class represents an undefined and unspecified entry node. + # This class represents an unspecified entry node. # # It decorates original entry adding method that indicates it is # unspecified. # class Undefined < SimpleDelegator - def initialize(entry) - super - end - def specified? false end diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb index 23d5faf6f07..e20908ad3cb 100644 --- a/lib/gitlab/ci/config/node/validators.rb +++ b/lib/gitlab/ci/config/node/validators.rb @@ -44,15 +44,6 @@ module Gitlab end end - class RequiredValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - if value.nil? - raise Entry::InvalidError, - "Entry needs #{attribute} attribute set internally." - end - end - end - class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb index beed29b18ae..c09a0a9c793 100644 --- a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::Ci::Config::Node::Artifacts do let(:config) { { paths: %w[public/] } } describe '#value' do - it 'returns image string' do + it 'returns artifacs configuration' do expect(entry.value).to eq config end end diff --git a/spec/lib/gitlab/ci/config/node/while_spec.rb b/spec/lib/gitlab/ci/config/node/trigger_spec.rb similarity index 90% rename from spec/lib/gitlab/ci/config/node/while_spec.rb rename to spec/lib/gitlab/ci/config/node/trigger_spec.rb index aac2ed7b3db..a4a3e36754e 100644 --- a/spec/lib/gitlab/ci/config/node/while_spec.rb +++ b/spec/lib/gitlab/ci/config/node/trigger_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::While do +describe Gitlab::Ci::Config::Node::Trigger do let(:entry) { described_class.new(config) } describe 'validations' do @@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Node::While do describe '#errors' do it 'saves errors' do expect(entry.errors) - .to include 'while config should be an array of strings or regexps' + .to include 'trigger config should be an array of strings or regexps' end end end From 7658f31a621f6ebc97af5c21802854ed56b0bd0f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 29 Jul 2016 07:28:34 -0700 Subject: [PATCH 123/198] Clarify backup_keep_time config parameter with S3 Discussed in gitlab-org/omnibus-gitlab#1453 [ci skip] --- doc/raketasks/backup_restore.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index fa976134341..5fa96736d59 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -382,6 +382,13 @@ backups using all your disk space. To do this add the following lines to gitlab_rails['backup_keep_time'] = 604800 ``` +Note that the `backup_keep_time` configuration option only manages local +files. GitLab does not automatically prune old files stored in a third-party +object storage (e.g. AWS S3) because the user may not have permission to list +and delete files. We recommend that you configure the appropriate retention +policy for your object storage. For example, you can configure [the S3 backup +policy here as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3). + NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). ## Alternative backup strategies From 60529e021667194c402d4f6e85a83e02a8bb9f75 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 29 Jul 2016 11:14:53 -0700 Subject: [PATCH 124/198] Properly abort a merge when merge conflicts occur If somehow a user attempted to accept a merge request that had conflicts (e.g. the "Accept Merge Request" button or the MR itself was not updated), `MergeService` did not properly detect that a conflict occurred. It would assume that the MR went through without any issues and close the MR as though everything was fine. This could cause data loss if the source branch were removed. Closes #20425 --- CHANGELOG | 1 + app/services/merge_requests/merge_service.rb | 8 +++++++- spec/services/merge_requests/merge_service_spec.rb | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 31a7eae90b9..6f7cf029440 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,7 @@ v 8.11.0 (unreleased) v 8.10.3 (unreleased) - Fix hooks missing on imported GitLab projects + - Properly abort a merge when merge conflicts occur v 8.10.2 - User can now search branches by name. !5144 diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 0dac0614141..b037780c431 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -35,7 +35,13 @@ module MergeRequests } commit_id = repository.merge(current_user, merge_request, options) - merge_request.update(merge_commit_sha: commit_id) + + if commit_id + merge_request.update(merge_commit_sha: commit_id) + else + merge_request.update(merge_error: 'Conflicts detected during merge') + false + end rescue GitHooksService::PreReceiveError => e merge_request.update(merge_error: e.message) false diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index f5bf3c1e367..8ffebcac698 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -75,6 +75,17 @@ describe MergeRequests::MergeService, services: true do expect(merge_request.merge_error).to eq("error") end + + it 'aborts if there is a merge conflict' do + allow_any_instance_of(Repository).to receive(:merge).and_return(false) + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request.open?).to be_truthy + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to eq("Conflicts detected during merge") + end end end end From 1b72256fa14e65256d78347f81b289d43c44e991 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 29 Jul 2016 12:14:36 +0200 Subject: [PATCH 125/198] Use Grape DSL for environment endpoints Also a couple of minor edits for this branch are included --- config/routes.rb | 2 +- doc/api/enviroments.md | 64 ++++++++--------- lib/api/api.rb | 4 ++ lib/api/environments.rb | 99 ++++++++++++-------------- spec/models/environment_spec.rb | 1 + spec/requests/api/environments_spec.rb | 57 +++++++++------ 6 files changed, 119 insertions(+), 108 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index ced204be7f7..371eb4bee7f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -741,7 +741,7 @@ Rails.application.routes.draw do end end - resources :environments, constraints: { id: /\d+/ } + resources :environments resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index 16bf2627fef..1e12ded448c 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -32,7 +32,7 @@ Example response: Creates a new environment with the given name and external_url. -It returns 200 if the environment was successfully created, 400 for wrong parameters. +It returns 201 if the environment was successfully created, 400 for wrong parameters. ``` POST /projects/:id/environment @@ -58,6 +58,37 @@ Example response: } ``` +## Edit an existing environment + +Updates an existing environment's name and/or external_url. + +It returns 200 if the environment was successfully updated. In case of an error, a status code 400 is returned. + +``` +PUT /projects/:id/environments/:environments_id +``` + +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer | yes | The ID of the project | +| `environment_id` | integer | yes | The ID of the environment | The ID of the environment | +| `name` | string | no | The new name of the environment | +| `external_url` | string | no | The new external_url | + +```bash +curl -X PUT --data "name=staging&external_url=https://staging.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "staging", + "external_url": "https://staging.example.gitlab.com" +} +``` + ## Delete an environment It returns 200 if the environment was successfully deleted, and 404 if the environment does not exist. @@ -84,34 +115,3 @@ Example response: "external_url": "https://deploy.example.gitlab.com" } ``` - -## Edit an existing environment - -Updates an existing environment's name and/or external_url. - -It returns 200 if the label was successfully updated, In case of an error, an additional error message is returned. - -``` -PUT /projects/:id/environments/:environments_id -``` - -| Attribute | Type | Required | Description | -| --------------- | ------- | --------------------------------- | ------------------------------- | -| `id` | integer | yes | The ID of the project | -| `environment_id` | integer | yes | The ID of the environment | The ID of the environment | -| `name` | string | no | The new name of the environment | -| `external_url` | string | no | The new external_url | - -```bash -curl -X PUT --data "name=staging&external_url=https://staging.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" -``` - -Example response: - -```json -{ - "id": 1, - "name": "staging", - "external_url": "https://staging.example.gitlab.com" -} -``` diff --git a/lib/api/api.rb b/lib/api/api.rb index 9c960d74495..bd16806892b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -7,6 +7,10 @@ module API rack_response({ 'message' => '404 Not found' }.to_json, 404) end + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!({ messages: e.full_messages }, 400) + end + rescue_from :all do |exception| # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 # why is this not wrapped in something reusable? diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 532baec42c7..a50f007d697 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -3,51 +3,70 @@ module API class Environments < Grape::API before { authenticate! } + params do + requires :id, type: String, desc: 'The project ID' + end resource :projects do - # Get all labels of the project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/environments + desc 'Get all environments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end get ':id/environments' do authorize! :read_environment, user_project present paginate(user_project.environments), with: Entities::Environment end - # Creates a new environment - # - # Parameters: - # id (required) - The ID of a project - # name (required) - The name of the environment to be created - # external_url (optional) - URL on which this deployment is viewable - # - # Example Request: - # POST /projects/:id/labels + desc 'Creates a new environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :name, type: String, desc: 'The name of the environment to be created' + optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + end post ':id/environments' do authorize! :create_environment, user_project - required_attributes! [:name] - attrs = attributes_for_keys [:name, :external_url] + create_params = declared(params, include_parent_namespaces: false).to_h + environment = user_project.environments.create(create_params) - environment = user_project.environments.create(attrs) - - if environment.valid? + if environment.persisted? present environment, with: Entities::Environment else render_validation_error!(environment) end end - # Deletes an existing environment - # - # Parameters: - # id (required) - The ID of a project - # environment_id (required) - The name of the environment to be deleted - # - # Example Request: - # DELETE /projects/:id/environments/:environment_id + desc 'Updates an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + optional :name, type: String, desc: 'The new environment name' + optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' + end + put ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + update_params = declared(params, include_missing: false).extract!(:name, :external_url).to_h + if environment.update(update_params) + present environment, with: Entities::Environment + else + render_validation_error!(environment) + end + end + + desc 'Deletes an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end delete ':id/environments/:environment_id' do authorize! :update_environment, user_project @@ -55,30 +74,6 @@ module API present environment.destroy, with: Entities::Environment end - - # Updates an existing environment - # - # Parameters: - # id (required) - The ID of a project - # environment_id (required) - The ID of the environment - # name (optional) - The name of the label to be deleted - # external_url (optional) - The new name of the label - # - # Example Request: - # PUT /projects/:id/environments/:environment_id - put ':id/environments/:environment_id' do - authorize! :update_environment, user_project - - environment = user_project.environments.find(params[:environment_id]) - - attrs = attributes_for_keys [:name, :external_url] - - if environment.update(attrs) - present environment, with: Entities::Environment - else - render_validation_error!(environment) - end - end end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index ef2148be1bd..8a84ac0a7c7 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -27,6 +27,7 @@ describe Environment, models: true do env = build(:environment, external_url: "") expect(env.save).to be true + expect(env.external_url).to be_nil end end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index b731c58a206..d315e456dda 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -14,7 +14,7 @@ describe API::API, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do - it 'should return project environments' do + it 'returns project environments' do get api("/projects/#{project.id}/environments", user) expect(response).to have_http_status(200) @@ -26,7 +26,7 @@ describe API::API, api: true do end context 'as non member' do - it 'should return a 404 status code' do + it 'returns a 404 status code' do get api("/projects/#{project.id}/environments", non_member) expect(response).to have_http_status(404) @@ -50,7 +50,7 @@ describe API::API, api: true do expect(response).to have_http_status(400) end - it 'should return 400 if environment already exists' do + it 'returns a 400 if environment already exists' do post api("/projects/#{project.id}/environments", user), name: environment.name expect(response).to have_http_status(400) @@ -61,20 +61,48 @@ describe API::API, api: true do it 'rejects the request' do post api("/projects/#{project.id}/environments", non_member) - expect(response).to have_http_status(404) + expect(response).to have_http_status(400) end end end + describe 'PUT /projects/:id/environments/:environment_id' do + it 'returns a 200 if name and external_url are changed' do + url = 'https://mepmep.whatever.ninja' + put api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep', external_url: url + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it "won't update the external_url if only the name is passed" do + url = environment.external_url + put api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it 'returns a 404 if the environment does not exist' do + put api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + end + end + describe 'DELETE /projects/:id/environments/:environment_id' do context 'as a master' do - it 'should return 200 for an existing environment' do + it 'returns a 200 for an existing environment' do delete api("/projects/#{project.id}/environments/#{environment.id}", user) expect(response).to have_http_status(200) end - it 'should return 404 for non existing id' do + it 'returns a 404 for non existing id' do delete api("/projects/#{project.id}/environments/12345", user) expect(response).to have_http_status(404) @@ -82,21 +110,4 @@ describe API::API, api: true do end end end - - describe 'PUT /projects/:id/environments/:environment_id' do - it 'should return 200 if name and external_url are changed' do - put api("/projects/#{project.id}/environments/#{environment.id}", user), - name: 'Mepmep', external_url: 'https://mepmep.whatever.ninja' - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq('Mepmep') - expect(json_response['external_url']).to eq('https://mepmep.whatever.ninja') - end - - it 'should return 404 if the environment does not exist' do - put api("/projects/#{project.id}/environments/12345", user) - - expect(response).to have_http_status(404) - end - end end From a54419f01f6cad33138e3d4ed049741699251f85 Mon Sep 17 00:00:00 2001 From: winniehell Date: Sun, 24 Jul 2016 07:32:47 +0200 Subject: [PATCH 126/198] Make "New issue" button in Issue page less obtrusive (!5457) --- CHANGELOG | 1 + app/views/help/ui.html.haml | 2 +- app/views/projects/issues/show.html.haml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 31a7eae90b9..641d6aa0d9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 8.11.0 (unreleased) - Bump gitlab_git to speedup DiffCollection iterations - Make branches sortable without push permission !5462 (winniehell) - Check for Ci::Build artifacts at database level on pipeline partial + - Make "New issue" button in Issue page less obtrusive !5457 (winniehell) - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 431d312b4ca..85e188d6f8b 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -189,7 +189,7 @@ %li %a Sort by date - = link_to 'New issue', '#', class: 'btn btn-new' + = link_to 'New issue', '#', class: 'btn btn-new btn-inverted' .lead Only nav links without button and search diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 9b6a97c0959..e5cce16a171 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -38,7 +38,7 @@ %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' From 9eb100241e2d18788523a24a3e9a6371f9bfb2a6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 29 Jul 2016 16:23:13 -0700 Subject: [PATCH 127/198] Clarify which project is deleted to avoid alarm Closes #13654 --- app/controllers/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ec7a2e63b9a..a6e1aa5ccc1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController end if @project.pending_delete? - flash[:alert] = "Project queued for delete." + flash[:alert] = "Project #{@project.name} queued for deletion." end respond_to do |format| From 48ff40a047103bf09d4ac53fdbc984d73bc464cb Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 29 Jul 2016 21:04:04 -0700 Subject: [PATCH 128/198] Improve diff performance by eliminating redundant checks for text blobs On a merge request with over 1000 changed files, there were redundant calls to blob_text_viewable?, which incurred about 7% of the time. Improves #14775 --- CHANGELOG | 1 + app/helpers/blob_helper.rb | 2 +- app/views/projects/blob/_actions.html.haml | 3 ++- app/views/projects/diffs/_file.html.haml | 9 +++++---- spec/helpers/blob_helper_spec.rb | 18 ++++++++++++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 31a7eae90b9..a4bb72a9221 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.11.0 (unreleased) - Fix the title of the toggle dropdown button. !5515 (herminiotorres) + - Improve diff performance by eliminating redundant checks for text blobs - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Fix CI status icon link underline (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index abe115d8c68..48c27828219 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -13,7 +13,7 @@ module BlobHelper blob = project.repository.blob_at(ref, path) rescue nil - return unless blob && blob_text_viewable?(blob) + return unless blob from_mr = options[:from_merge_request_id] link_opts = {} diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index cdac50f7a8d..ff893ea74e1 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -16,6 +16,7 @@ - if current_user .btn-group{ role: "group" } - = edit_blob_link + - if blob_text_viewable?(@blob) + = edit_blob_link = replace_blob_link = delete_blob_link diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c306909fb1a..1854c64cbd7 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -9,10 +9,11 @@ = icon('comment') \ - - if editable_diff?(diff_file) - = edit_blob_link(@merge_request.source_project, - @merge_request.source_branch, diff_file.new_path, - from_merge_request_id: @merge_request.id) + - if editable_diff?(diff_file) + = edit_blob_link(@merge_request.source_project, + @merge_request.source_branch, diff_file.new_path, + from_merge_request_id: @merge_request.id, + skip_visible_check: true) = view_file_btn(diff_commit.id, diff_file, project) diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index bd0108f9938..b2d6d59b1ee 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BlobHelper do + include TreeHelper + let(:blob_name) { 'test.lisp' } let(:no_context_content) { ":type \"assem\"))" } let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } @@ -65,4 +67,20 @@ describe BlobHelper do expect(sanitize_svg(blob).data).to eq(expected) end end + + describe "#edit_blob_link" do + let(:project) { create(:project) } + + before do + allow(self).to receive(:current_user).and_return(double) + end + + it 'verifies blob is text' do + expect(self).not_to receive(:blob_text_viewable?) + + button = edit_blob_link(project, 'refs/heads/master', 'README.md') + + expect(button).to start_with(' Date: Sun, 31 Jul 2016 15:36:11 -0400 Subject: [PATCH 129/198] Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. --- CHANGELOG | 1 + config/initializers/trusted_proxies.rb | 2 ++ spec/initializers/trusted_proxies_spec.rb | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9b66108c160..a0a3484d9a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,7 @@ v 8.11.0 (unreleased) - Make error pages responsive (Takuya Noguchi) - Change requests_profiles resource constraint to catch virtually any file - Reduce number of queries made for merge_requests/:id/diffs + - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. v 8.10.3 (unreleased) - Fix hooks missing on imported GitLab projects diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb index 30770b71e24..cd869657c53 100644 --- a/config/initializers/trusted_proxies.rb +++ b/config/initializers/trusted_proxies.rb @@ -7,6 +7,8 @@ module Rack class Request def trusted_proxy?(ip) Rails.application.config.action_dispatch.trusted_proxies.any? { |proxy| proxy === ip } + rescue IPAddr::InvalidAddressError + false end end end diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb index 52d5a7dffc9..290e47763eb 100644 --- a/spec/initializers/trusted_proxies_spec.rb +++ b/spec/initializers/trusted_proxies_spec.rb @@ -47,6 +47,12 @@ describe 'trusted_proxies', lib: true do expect(request.remote_ip).to eq('1.1.1.1') expect(request.ip).to eq('1.1.1.1') end + + it 'handles invalid ip addresses' do + request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 1.1.1.1:12345, 1.1.1.1') + expect(request.remote_ip).to eq('1.1.1.1') + expect(request.ip).to eq('1.1.1.1') + end end def stub_request(headers = {}) From c9ce36e829be6a5991996a495946fe9416747c6e Mon Sep 17 00:00:00 2001 From: lookatmike Date: Sun, 31 Jul 2016 16:18:06 -0400 Subject: [PATCH 130/198] Moved to 8.10.3 release. --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a0a3484d9a2..9075972e6d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,11 +36,11 @@ v 8.11.0 (unreleased) - Make error pages responsive (Takuya Noguchi) - Change requests_profiles resource constraint to catch virtually any file - Reduce number of queries made for merge_requests/:id/diffs - - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. v 8.10.3 (unreleased) - Fix hooks missing on imported GitLab projects - Properly abort a merge when merge conflicts occur + - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. v 8.10.2 - User can now search branches by name. !5144 From 5b4ceeed6317cc8039642981ba356565e11d991e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 29 Jul 2016 18:24:11 -0300 Subject: [PATCH 131/198] Fix attr reader to force the intended values for source and target shas When importing a pull request from GitHub, the old and new branches may no longer actually exist by those names, but we need to recreate the merge request diff with the right source and target shas. We use these `target_branch_sha` and `source_branch_sha` attributes to force these to the intended values. But the reader methods were always looking up to the target/source branch head instead of check if these values was previously set. --- app/models/merge_request.rb | 4 ++++ spec/models/merge_request_spec.rb | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 471e32f3b60..fdcbbdc1d08 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -238,10 +238,14 @@ class MergeRequest < ActiveRecord::Base end def target_branch_sha + return @target_branch_sha if defined?(@target_branch_sha) + target_branch_head.try(:sha) end def source_branch_sha + return @source_branch_sha if defined?(@source_branch_sha) + source_branch_head.try(:sha) end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c8ad7ab3e7f..a0e3c26e542 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -65,11 +65,11 @@ describe MergeRequest, models: true do end describe '#target_branch_sha' do - context 'when the target branch does not exist anymore' do - let(:project) { create(:project) } + let(:project) { create(:project) } - subject { create(:merge_request, source_project: project, target_project: project) } + subject { create(:merge_request, source_project: project, target_project: project) } + context 'when the target branch does not exist' do before do project.repository.raw_repository.delete_branch(subject.target_branch) end @@ -78,6 +78,12 @@ describe MergeRequest, models: true do expect(subject.target_branch_sha).to be_nil end end + + it 'returns memoized value' do + subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7' + + expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7' + end end describe '#source_branch_sha' do @@ -103,6 +109,12 @@ describe MergeRequest, models: true do expect(subject.source_branch_sha).to be_nil end end + + it 'returns memoized value' do + subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + + expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end end describe '#to_reference' do From 849e8e0c371e4994ae6a47e8e91470e7bb1eaf18 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 29 Jul 2016 18:35:46 -0300 Subject: [PATCH 132/198] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 9b66108c160..716665157bf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,7 @@ v 8.11.0 (unreleased) - Reduce number of queries made for merge_requests/:id/diffs v 8.10.3 (unreleased) + - Fix importer for GitHub Pull Requests when a branch was removed - Fix hooks missing on imported GitLab projects - Properly abort a merge when merge conflicts occur From 285ba1b20f226f0bf7ab01010b64cabdccecf096 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Sun, 31 Jul 2016 19:44:02 -0300 Subject: [PATCH 133/198] fixup! Fix attr reader to force the intended values for source and target shas --- app/models/merge_request.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index fdcbbdc1d08..f1b9c1d6feb 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -238,15 +238,11 @@ class MergeRequest < ActiveRecord::Base end def target_branch_sha - return @target_branch_sha if defined?(@target_branch_sha) - - target_branch_head.try(:sha) + @target_branch_sha || target_branch_head.try(:sha) end def source_branch_sha - return @source_branch_sha if defined?(@source_branch_sha) - - source_branch_head.try(:sha) + @source_branch_sha || source_branch_head.try(:sha) end def diff_refs From 0fa50494b8f1c765f7c046e0648afec81c27dcd7 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 31 Jul 2016 22:14:48 -0700 Subject: [PATCH 134/198] Improve spinach test to be more specific about link to click If you add another branch to gitlab-test that includes the word 'test', browse_files.feature will fail with an ambiguous match. --- features/steps/project/source/browse_files.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 0fe046dcbf6..9a8896acb15 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -293,7 +293,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps first('.js-project-refs-dropdown').click page.within '.project-refs-form' do - click_link 'test' + click_link "'test'" end end From 34c1c8a3b14ab3b29fbde97532c89404d9573a1d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 1 Aug 2016 08:42:09 +0200 Subject: [PATCH 135/198] Minor fixes in the Env API endpoints --- .../projects/environments_controller.rb | 2 +- lib/api/environments.rb | 6 +++++- spec/requests/api/environments_spec.rb | 20 +++++++++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 1f5c7506212..58678f96879 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:edit, :destroy] + before_action :authorize_update_environment!, only: [:edit, :update, :destroy] before_action :environment, only: [:show, :edit, :update, :destroy] def index diff --git a/lib/api/environments.rb b/lib/api/environments.rb index a50f007d697..819f80d8365 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -11,6 +11,10 @@ module API detail 'This feature was introduced in GitLab 8.11.' success Entities::Environment end + params do + optional :page, type: Integer, desc: 'Page number of the current request' + optional :per_page, type: Integer, desc: 'Number of items per page' + end get ':id/environments' do authorize! :read_environment, user_project @@ -51,7 +55,7 @@ module API authorize! :update_environment, user_project environment = user_project.environments.find(params[:environment_id]) - + update_params = declared(params, include_missing: false).extract!(:name, :external_url).to_h if environment.update(update_params) present environment, with: Entities::Environment diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index d315e456dda..05e57905343 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -14,6 +14,10 @@ describe API::API, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/environments", user) } + end + it 'returns project environments' do get api("/projects/#{project.id}/environments", user) @@ -59,9 +63,13 @@ describe API::API, api: true do context 'a non member' do it 'rejects the request' do - post api("/projects/#{project.id}/environments", non_member) + post api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com' - expect(response).to have_http_status(400) + expect(response).to have_http_status(404) + end + + it 'returns a 400 when the required params are missing' do + post api("/projects/12345/environments", non_member), external_url: 'http://env.git.com' end end end @@ -109,5 +117,13 @@ describe API::API, api: true do expect(json_response['message']).to eq('404 Not found') end end + + context 'a non member' do + it 'rejects the request' do + delete api("/projects/#{project.id}/environments/#{environment.id}", non_member) + + expect(response).to have_http_status(404) + end + end end end From 52bb564812d106124b95c93f5a502f3ced9c280b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 27 Jul 2016 17:56:25 +0200 Subject: [PATCH 136/198] squashed - fix timing issues in prod importing projects added changelog fix specs refactored code based on feedback fix rubocop warning --- CHANGELOG | 1 + lib/gitlab/import_export/file_importer.rb | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9075972e6d0..4095eaa6e63 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,6 +41,7 @@ v 8.10.3 (unreleased) - Fix hooks missing on imported GitLab projects - Properly abort a merge when merge conflicts occur - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. + - Fix timing problems running imports on production v 8.10.2 - User can now search branches by name. !5144 diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index 82d1e1805c5..ff7174c995f 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -3,6 +3,8 @@ module Gitlab class FileImporter include Gitlab::ImportExport::CommandLineUtil + MAX_RETRIES = 8 + def self.import(*args) new(*args).import end @@ -14,7 +16,10 @@ module Gitlab def import FileUtils.mkdir_p(@shared.export_path) - decompress_archive + + wait_for_archived_file do + decompress_archive + end rescue => e @shared.error(e) false @@ -22,6 +27,19 @@ module Gitlab private + # Exponentially sleep until I/O finishes copying the file + def wait_for_archived_file + MAX_RETRIES.times do |retry_number| + if File.exist?(@archive_file) + yield + + break + else + sleep(2**retry_number) + end + end + end + def decompress_archive result = untar_zxf(archive: @archive_file, dir: @shared.export_path) From dad1d0b8646b75ffcb3b1f6758848c53482f6bd2 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 29 Jul 2016 15:27:21 +0200 Subject: [PATCH 137/198] fix return value and spec --- lib/gitlab/import_export/file_importer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index ff7174c995f..4b5f1f26286 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -33,7 +33,7 @@ module Gitlab if File.exist?(@archive_file) yield - break + return true else sleep(2**retry_number) end From aad0ae71620d8e988faf75587a612b933df00366 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 22 Jul 2016 12:10:45 +0200 Subject: [PATCH 138/198] squashed - fixed label and milestone association problems, updated specs and refactored reader class a bit --- CHANGELOG | 1 + app/models/label_link.rb | 6 +- app/models/project.rb | 10 + lib/gitlab/import_export.rb | 2 +- lib/gitlab/import_export/import_export.yml | 22 ++- lib/gitlab/import_export/json_hash_builder.rb | 110 +++++++++++ .../import_export/project_tree_restorer.rb | 4 +- lib/gitlab/import_export/reader.rb | 77 +------- lib/gitlab/import_export/relation_factory.rb | 53 +++++- spec/lib/gitlab/import_export/project.json | 176 +++++------------- .../project_tree_restorer_spec.rb | 12 ++ .../import_export/project_tree_saver_spec.rb | 30 +-- 12 files changed, 266 insertions(+), 237 deletions(-) create mode 100644 lib/gitlab/import_export/json_hash_builder.rb diff --git a/CHANGELOG b/CHANGELOG index 9075972e6d0..3537b147b2a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,6 +41,7 @@ v 8.10.3 (unreleased) - Fix hooks missing on imported GitLab projects - Properly abort a merge when merge conflicts occur - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. + - Fix Import/Export issue importing milestones and labels not associated properly v 8.10.2 - User can now search branches by name. !5144 diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 47bd6eaf35f..51b5c2b1f4c 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -1,7 +1,9 @@ class LabelLink < ActiveRecord::Base + include Importable + belongs_to :target, polymorphic: true belongs_to :label - validates :target, presence: true - validates :label, presence: true + validates :target, presence: true, unless: :importing? + validates :label, presence: true, unless: :importing? end diff --git a/app/models/project.rb b/app/models/project.rb index 7aecd7860c5..83b848ded8b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1253,6 +1253,16 @@ class Project < ActiveRecord::Base authorized_for_user_by_shared_projects?(user, min_access_level) end + def append_or_update_attribute(name, value) + old_values = public_send(name.to_s) + + if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any? + update_attribute(name, old_values + value) + else + update_attribute(name, value) + end + end + private def authorized_for_user_by_group?(user, min_access_level) diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index d6d14bd98a0..48b2c43ac21 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -2,7 +2,7 @@ module Gitlab module ImportExport extend self - VERSION = '0.1.2' + VERSION = '0.1.3' FILENAME_LIMIT = 50 def export_path(relative_path:) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 15afe8174a4..1da51043611 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -3,11 +3,12 @@ project_tree: - issues: - :events - notes: - - :author - - :events - - :labels - - milestones: - - :events + - :author + - :events + - label_links: + - :label + - milestone: + - :events - snippets: - notes: :author @@ -20,6 +21,10 @@ project_tree: - :events - :merge_request_diff - :events + - label_links: + - :label + - milestone: + - :events - pipelines: - notes: - :author @@ -31,6 +36,9 @@ project_tree: - :services - :hooks - :protected_branches + - :labels + - milestones: + - :events # Only include the following attributes for the models specified. included_attributes: @@ -55,6 +63,10 @@ excluded_attributes: - :expired_at merge_request_diff: - :st_diffs + issues: + - :milestone_id + merge_requests: + - :milestone_id methods: statuses: diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb new file mode 100644 index 00000000000..008300bde45 --- /dev/null +++ b/lib/gitlab/import_export/json_hash_builder.rb @@ -0,0 +1,110 @@ +module Gitlab + module ImportExport + # Generates a hash that conforms with http://apidock.com/rails/Hash/to_json + # and its peculiar options. + class JsonHashBuilder + def self.build(model_objects, attributes_finder) + new(model_objects, attributes_finder).build + end + + def initialize(model_objects, attributes_finder) + @model_objects = model_objects + @attributes_finder = attributes_finder + end + + def build + process_model_objects(@model_objects) + end + + private + + # Called when the model is actually a hash containing other relations (more models) + # Returns the config in the right format for calling +to_json+ + # + # +model_object_hash+ - A model relationship such as: + # {:merge_requests=>[:merge_request_diff, :notes]} + def process_model_objects(model_object_hash) + json_config_hash = {} + current_key = model_object_hash.keys.first + + model_object_hash.values.flatten.each do |model_object| + @attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash } + handle_model_object(current_key, model_object, json_config_hash) + end + + json_config_hash + end + + # Creates or adds to an existing hash an individual model or list + # + # +current_key+ main model that will be a key in the hash + # +model_object+ model or list of models to include in the hash + # +json_config_hash+ the original hash containing the root model + def handle_model_object(current_key, model_object, json_config_hash) + model_or_sub_model = model_object.is_a?(Hash) ? process_model_objects(model_object) : model_object + + if json_config_hash[current_key] + add_model_value(current_key, model_or_sub_model, json_config_hash) + else + create_model_value(current_key, model_or_sub_model, json_config_hash) + end + end + + # Constructs a new hash that will hold the configuration for that particular object + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + # +json_config_hash+ the original hash containing the root model + def create_model_value(current_key, value, json_config_hash) + parsed_hash = { include: value } + parse_hash(value, parsed_hash) + + json_config_hash[current_key] = parsed_hash + end + + # Calls attributes finder to parse the hash and add any attributes to it + # + # +value+ existing model to be included in the hash + # +parsed_hash+ the original hash + def parse_hash(value, parsed_hash) + @attributes_finder.parse(value) do |hash| + parsed_hash = { include: hash_or_merge(value, hash) } + end + end + + # Adds new model configuration to an existing hash with key +current_key+ + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + # +json_config_hash+ the original hash containing the root model + def add_model_value(current_key, value, json_config_hash) + @attributes_finder.parse(value) { |hash| value = { value => hash } } + + add_to_array(current_key, json_config_hash, value) + end + + # Adds new model configuration to an existing hash with key +current_key+ + # it creates a new array if it was previously a single value + # + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash + # +json_config_hash+ the original hash containing the root model + def add_to_array(current_key, json_config_hash, value) + old_values = json_config_hash[current_key][:include] + + json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten + end + + # Construct a new hash or merge with an existing one a model configuration + # This is to fulfil +to_json+ requirements. + # + # +hash+ hash containing configuration generated mainly from +@attributes_finder+ + # +value+ existing model to be included in the hash + def hash_or_merge(value, hash) + value.is_a?(Hash) ? value.merge(hash) : { value => hash } + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 051110c23cf..c7b3551b84c 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -47,7 +47,7 @@ module Gitlab relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s]) - saved << restored_project.update_attribute(relation_key, relation_hash) + saved << restored_project.append_or_update_attribute(relation_key, relation_hash) end saved.all? end @@ -78,7 +78,7 @@ module Gitlab relation_key = relation.keys.first.to_s return if tree_hash[relation_key].blank? - tree_hash[relation_key].each do |relation_item| + [tree_hash[relation_key]].flatten.each do |relation_item| relation.values.flatten.each do |sub_relation| # We just use author to get the user ID, do not attempt to create an instance. next if sub_relation == :author diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb index 15f5dd31035..5021a1a14ce 100644 --- a/lib/gitlab/import_export/reader.rb +++ b/lib/gitlab/import_export/reader.rb @@ -29,87 +29,12 @@ module Gitlab def build_hash(model_list) model_list.map do |model_objects| if model_objects.is_a?(Hash) - build_json_config_hash(model_objects) + Gitlab::ImportExport::JsonHashBuilder.build(model_objects, @attributes_finder) else @attributes_finder.find(model_objects) end end end - - # Called when the model is actually a hash containing other relations (more models) - # Returns the config in the right format for calling +to_json+ - # +model_object_hash+ - A model relationship such as: - # {:merge_requests=>[:merge_request_diff, :notes]} - def build_json_config_hash(model_object_hash) - @json_config_hash = {} - - model_object_hash.values.flatten.each do |model_object| - current_key = model_object_hash.keys.first - - @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash } - - handle_model_object(current_key, model_object) - process_sub_model(current_key, model_object) if model_object.is_a?(Hash) - end - @json_config_hash - end - - # If the model is a hash, process the sub_models, which could also be hashes - # If there is a list, add to an existing array, otherwise use hash syntax - # +current_key+ main model that will be a key in the hash - # +model_object+ model or list of models to include in the hash - def process_sub_model(current_key, model_object) - sub_model_json = build_json_config_hash(model_object).dup - @json_config_hash.slice!(current_key) - - if @json_config_hash[current_key] && @json_config_hash[current_key][:include] - @json_config_hash[current_key][:include] << sub_model_json - else - @json_config_hash[current_key] = { include: sub_model_json } - end - end - - # Creates or adds to an existing hash an individual model or list - # +current_key+ main model that will be a key in the hash - # +model_object+ model or list of models to include in the hash - def handle_model_object(current_key, model_object) - if @json_config_hash[current_key] - add_model_value(current_key, model_object) - else - create_model_value(current_key, model_object) - end - end - - # Constructs a new hash that will hold the configuration for that particular object - # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ - # +current_key+ main model that will be a key in the hash - # +value+ existing model to be included in the hash - def create_model_value(current_key, value) - parsed_hash = { include: value } - - @attributes_finder.parse(value) do |hash| - parsed_hash = { include: hash_or_merge(value, hash) } - end - @json_config_hash[current_key] = parsed_hash - end - - # Adds new model configuration to an existing hash with key +current_key+ - # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ - # +current_key+ main model that will be a key in the hash - # +value+ existing model to be included in the hash - def add_model_value(current_key, value) - @attributes_finder.parse(value) { |hash| value = { value => hash } } - old_values = @json_config_hash[current_key][:include] - @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten - end - - # Construct a new hash or merge with an existing one a model configuration - # This is to fulfil +to_json+ requirements. - # +value+ existing model to be included in the hash - # +hash+ hash containing configuration generated mainly from +@attributes_finder+ - def hash_or_merge(value, hash) - value.is_a?(Hash) ? value.merge(hash) : { value => hash } - end end end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index e41c7e6bf4f..e9c1b79fa45 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -13,6 +13,10 @@ module Gitlab BUILD_MODELS = %w[Ci::Build commit_status].freeze + IMPORTED_OBJECT_MAX_RETRIES = 5.freeze + + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze + def self.create(*args) new(*args).create end @@ -22,24 +26,35 @@ module Gitlab @relation_hash = relation_hash.except('id', 'noteable_id') @members_mapper = members_mapper @user = user + @imported_object_retries = 0 end # Creates an object from an actual model with name "relation_sym" with params from # the relation_hash, updating references with new object IDs, mapping users using # the "members_mapper" object, also updating notes if required. def create - set_note_author if @relation_name == :notes - update_user_references - update_project_references - reset_ci_tokens if @relation_name == 'Ci::Trigger' - @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] - set_st_diffs if @relation_name == :merge_request_diff + setup_models generate_imported_object end private + def setup_models + if @relation_name == :notes + set_note_author + + # TODO: note attatchments not supported yet + @relation_hash['attachment'] = nil + end + + update_user_references + update_project_references + reset_ci_tokens if @relation_name == 'Ci::Trigger' + @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] + set_st_diffs if @relation_name == :merge_request_diff + end + def update_user_references USER_REFERENCES.each do |reference| if @relation_hash[reference] @@ -112,10 +127,14 @@ module Gitlab end def imported_object - imported_object = relation_class.new(parsed_relation_hash) - yield(imported_object) if block_given? - imported_object.importing = true if imported_object.respond_to?(:importing) - imported_object + yield(existing_or_new_object) if block_given? + existing_or_new_object.importing = true if existing_or_new_object.respond_to?(:importing) + existing_or_new_object + rescue ActiveRecord::RecordNotUnique + # as the operation is not atomic, retry in the unlikely scenario an INSERT is + # performed on the same object between the SELECT and the INSERT + @imported_object_retries += 1 + retry if @imported_object_retries < IMPORTED_OBJECT_MAX_RETRIES end def update_note_for_missing_author(author_name) @@ -134,6 +153,20 @@ module Gitlab def set_st_diffs @relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs') end + + def existing_or_new_object + # Only find existing records to avoid mapping tables such as milestones + # Otherwise always create the record, skipping the extra SELECT clause. + @existing_or_new_object ||= begin + if EXISTING_OBJECT_CHECK.include?(@relation_name) + existing_object = relation_class.find_or_initialize_by(parsed_relation_hash.slice('title', 'project_id')) + existing_object.assign_attributes(parsed_relation_hash) + existing_object + else + relation_class.new(parsed_relation_hash) + end + end + end end end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index b1a5d72c624..b5550ca1963 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -18,7 +18,6 @@ "position": 0, "branch_name": null, "description": "Aliquam enim illo et possimus.", - "milestone_id": 18, "state": "opened", "iid": 10, "updated_by_id": null, @@ -27,6 +26,52 @@ "due_date": null, "moved_to_id": null, "test_ee_field": "test", + "milestone": { + "id": 1, + "title": "v0.0", + "project_id": 8, + "description": "test milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "events": [ + { + "id": 487, + "target_type": "Milestone", + "target_id": 1, + "title": null, + "data": null, + "project_id": 46, + "created_at": "2016-06-14T15:02:04.418Z", + "updated_at": "2016-06-14T15:02:04.418Z", + "action": 1, + "author_id": 18 + } + ] + }, + "label_links": [ + { + "id": 2, + "label_id": 2, + "target_id": 3, + "target_type": "Issue", + "created_at": "2016-07-22T08:57:02.840Z", + "updated_at": "2016-07-22T08:57:02.840Z", + "label": { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "priority": null + } + } + ], "notes": [ { "id": 351, @@ -233,7 +278,6 @@ "position": 0, "branch_name": null, "description": "Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.", - "milestone_id": 16, "state": "opened", "iid": 9, "updated_by_id": null, @@ -447,7 +491,6 @@ "position": 0, "branch_name": null, "description": "Ea recusandae neque autem tempora.", - "milestone_id": 16, "state": "closed", "iid": 8, "updated_by_id": null, @@ -661,7 +704,6 @@ "position": 0, "branch_name": null, "description": "Maiores architecto quos in dolorem.", - "milestone_id": 17, "state": "opened", "iid": 7, "updated_by_id": null, @@ -875,7 +917,6 @@ "position": 0, "branch_name": null, "description": "Ut aut ut et tenetur velit aut id modi.", - "milestone_id": 16, "state": "opened", "iid": 6, "updated_by_id": null, @@ -1089,7 +1130,6 @@ "position": 0, "branch_name": null, "description": "Dicta nisi nihil non ipsa velit.", - "milestone_id": 20, "state": "closed", "iid": 5, "updated_by_id": null, @@ -1303,7 +1343,6 @@ "position": 0, "branch_name": null, "description": "Ut et explicabo vel voluptatem consequuntur ut sed.", - "milestone_id": 19, "state": "closed", "iid": 4, "updated_by_id": null, @@ -1517,7 +1556,6 @@ "position": 0, "branch_name": null, "description": "Non asperiores velit accusantium voluptate.", - "milestone_id": 18, "state": "closed", "iid": 3, "updated_by_id": null, @@ -1731,7 +1769,6 @@ "position": 0, "branch_name": null, "description": "Molestiae corporis magnam et fugit aliquid nulla quia.", - "milestone_id": 17, "state": "closed", "iid": 2, "updated_by_id": null, @@ -1945,7 +1982,6 @@ "position": 0, "branch_name": null, "description": "Quod ad architecto qui est sed quia.", - "milestone_id": 20, "state": "closed", "iid": 1, "updated_by_id": null, @@ -2259,117 +2295,6 @@ "author_id": 25 } ] - }, - { - "id": 18, - "title": "v2.0", - "project_id": 5, - "description": "Error dolorem rerum aut nulla.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.576Z", - "updated_at": "2016-06-14T15:02:04.576Z", - "state": "active", - "iid": 3, - "events": [ - { - "id": 242, - "target_type": "Milestone", - "target_id": 18, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.579Z", - "updated_at": "2016-06-14T15:02:04.579Z", - "action": 1, - "author_id": 1 - }, - { - "id": 58, - "target_type": "Milestone", - "target_id": 18, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.579Z", - "updated_at": "2016-06-14T15:02:04.579Z", - "action": 1, - "author_id": 22 - } - ] - }, - { - "id": 17, - "title": "v1.0", - "project_id": 5, - "description": "Molestiae perspiciatis voluptates doloremque commodi veniam consequatur.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.569Z", - "updated_at": "2016-06-14T15:02:04.569Z", - "state": "active", - "iid": 2, - "events": [ - { - "id": 243, - "target_type": "Milestone", - "target_id": 17, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.570Z", - "updated_at": "2016-06-14T15:02:04.570Z", - "action": 1, - "author_id": 1 - }, - { - "id": 57, - "target_type": "Milestone", - "target_id": 17, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.570Z", - "updated_at": "2016-06-14T15:02:04.570Z", - "action": 1, - "author_id": 20 - } - ] - }, - { - "id": 16, - "title": "v0.0", - "project_id": 5, - "description": "Velit numquam et sed sit.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.561Z", - "updated_at": "2016-06-14T15:02:04.561Z", - "state": "closed", - "iid": 1, - "events": [ - { - "id": 244, - "target_type": "Milestone", - "target_id": 16, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.563Z", - "updated_at": "2016-06-14T15:02:04.563Z", - "action": 1, - "author_id": 26 - }, - { - "id": 56, - "target_type": "Milestone", - "target_id": 16, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.563Z", - "updated_at": "2016-06-14T15:02:04.563Z", - "action": 1, - "author_id": 26 - } - ] } ], "snippets": [ @@ -2471,7 +2396,6 @@ "title": "Cannot be automatically merged", "created_at": "2016-06-14T15:02:36.568Z", "updated_at": "2016-06-14T15:02:56.815Z", - "milestone_id": null, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -2909,7 +2833,6 @@ "title": "Can be automatically merged", "created_at": "2016-06-14T15:02:36.418Z", "updated_at": "2016-06-14T15:02:57.013Z", - "milestone_id": null, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -3194,7 +3117,6 @@ "title": "Qui accusantium et inventore facilis doloribus occaecati officiis.", "created_at": "2016-06-14T15:02:25.168Z", "updated_at": "2016-06-14T15:02:59.521Z", - "milestone_id": 17, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -3479,7 +3401,6 @@ "title": "In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.", "created_at": "2016-06-14T15:02:24.760Z", "updated_at": "2016-06-14T15:02:59.749Z", - "milestone_id": 20, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -4170,7 +4091,6 @@ "title": "Voluptates consequatur eius nemo amet libero animi illum delectus tempore.", "created_at": "2016-06-14T15:02:24.415Z", "updated_at": "2016-06-14T15:02:59.958Z", - "milestone_id": 17, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -4719,7 +4639,6 @@ "title": "In a rerum harum nihil accusamus aut quia nobis non.", "created_at": "2016-06-14T15:02:24.000Z", "updated_at": "2016-06-14T15:03:00.225Z", - "milestone_id": 19, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -5219,7 +5138,6 @@ "title": "Corporis provident similique perspiciatis dolores eos animi.", "created_at": "2016-06-14T15:02:23.767Z", "updated_at": "2016-06-14T15:03:00.475Z", - "milestone_id": 18, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -5480,7 +5398,6 @@ "title": "Eligendi reprehenderit doloribus quia et sit id.", "created_at": "2016-06-14T15:02:23.014Z", "updated_at": "2016-06-14T15:03:00.685Z", - "milestone_id": 20, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -6171,7 +6088,6 @@ "title": "Et ipsam voluptas velit sequi illum ut.", "created_at": "2016-06-14T15:02:22.825Z", "updated_at": "2016-06-14T15:03:00.904Z", - "milestone_id": 16, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 6ae20c943b1..32c0d6462f1 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -60,6 +60,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) end + + it 'has labels associated to label links, associated to issues' do + restored_project_json + + expect(Label.first.label_links.first.target).not_to be_nil + end + + it 'has milestones associated to issues' do + restored_project_json + + expect(Milestone.find_by_description('test milestone').issues).not_to be_empty + end end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 057ef6e76a0..3a86a4ce07c 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -31,10 +31,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json).to include({ "visibility_level" => 20 }) end - it 'has events' do - expect(saved_project_json['milestones'].first['events']).not_to be_empty - end - it 'has milestones' do expect(saved_project_json['milestones']).not_to be_empty end @@ -43,8 +39,12 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests']).not_to be_empty end - it 'has labels' do - expect(saved_project_json['labels']).not_to be_empty + it 'has merge request\'s milestones' do + expect(saved_project_json['merge_requests'].first['milestone']).not_to be_empty + end + + it 'has events' do + expect(saved_project_json['merge_requests'].first['milestone']['events']).not_to be_empty end it 'has snippets' do @@ -103,6 +103,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['pipelines'].first['notes']).not_to be_empty end + it 'has labels with no associations' do + expect(saved_project_json['labels']).not_to be_empty + end + + it 'has labels associated to records' do + expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty + end + it 'does not complain about non UTF-8 characters in MR diffs' do ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") @@ -113,19 +121,19 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do def setup_project issue = create(:issue, assignee: user) - label = create(:label) snippet = create(:project_snippet) release = create(:release) project = create(:project, :public, issues: [issue], - labels: [label], snippets: [snippet], releases: [release] ) - - merge_request = create(:merge_request, source_project: project) + label = create(:label, project: project) + create(:label_link, label: label, target: issue) + milestone = create(:milestone, project: project) + merge_request = create(:merge_request, source_project: project, milestone: milestone) commit_status = create(:commit_status, project: project) ci_pipeline = create(:ci_pipeline, @@ -135,7 +143,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do statuses: [commit_status]) create(:ci_build, pipeline: ci_pipeline, project: project) - milestone = create(:milestone, project: project) + create(:milestone, project: project) create(:note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) create(:note, noteable: snippet, project: project) From 81495528f90b009bd4e649fbfd2b0ff951032fc4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 1 Aug 2016 11:07:06 +0200 Subject: [PATCH 139/198] refactored wait_for_archived_file method --- lib/gitlab/import_export/file_importer.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index 4b5f1f26286..eca6e5b6d51 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -30,14 +30,12 @@ module Gitlab # Exponentially sleep until I/O finishes copying the file def wait_for_archived_file MAX_RETRIES.times do |retry_number| - if File.exist?(@archive_file) - yield + break if File.exist?(@archive_file) - return true - else - sleep(2**retry_number) - end + sleep(2**retry_number) end + + yield end def decompress_archive From 84a3225b0cde0ed2e343864583e7b79d7118e05c Mon Sep 17 00:00:00 2001 From: zs Date: Sun, 24 Jul 2016 01:28:12 +0200 Subject: [PATCH 140/198] State specific default sort order for issuables Provide more sensible default sort order for issues and merge requests based on the following table: | type | state | default sort order | |----------------|--------|--------------------| | issues | open | last created | | issues | closed | last updated | | issues | all | last created | | merge requests | open | last created | | merge requests | merged | last updated | | merge requests | closed | last updated | | merge requests | all | last created | --- CHANGELOG | 1 + app/controllers/application_controller.rb | 56 ------ .../concerns/issuable_collections.rb | 79 ++++++++ app/controllers/concerns/issues_action.rb | 10 +- .../concerns/merge_requests_action.rb | 10 +- app/controllers/projects/issues_controller.rb | 3 +- .../projects/merge_requests_controller.rb | 3 +- app/finders/issuable_finder.rb | 2 +- app/helpers/application_helper.rb | 1 - app/helpers/sorting_helper.rb | 4 +- app/views/projects/issues/index.html.haml | 8 +- .../issuables/default_sort_order_spec.rb | 171 ++++++++++++++++++ spec/features/issues_spec.rb | 21 +-- .../user_lists_merge_requests_spec.rb | 33 ++-- spec/support/issue_helpers.rb | 13 ++ spec/support/merge_request_helpers.rb | 13 ++ 16 files changed, 322 insertions(+), 106 deletions(-) create mode 100644 app/controllers/concerns/issuable_collections.rb create mode 100644 spec/features/issuables/default_sort_order_spec.rb create mode 100644 spec/support/issue_helpers.rb create mode 100644 spec/support/merge_request_helpers.rb diff --git a/CHANGELOG b/CHANGELOG index 2b04c15b827..dc6bf3c0cc2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.11.0 (unreleased) - Make error pages responsive (Takuya Noguchi) - Change requests_profiles resource constraint to catch virtually any file - Reduce number of queries made for merge_requests/:id/diffs + - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) v 8.10.3 (unreleased) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a1004d9bcea..634d36a4467 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -243,42 +243,6 @@ class ApplicationController < ActionController::Base end end - def set_filters_params - set_default_sort - - params[:scope] = 'all' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? - - @sort = params[:sort] - @filter_params = params.dup - - if @project - @filter_params[:project_id] = @project.id - elsif @group - @filter_params[:group_id] = @group.id - else - # TODO: this filter ignore issues/mr created in public or - # internal repos where you are not a member. Enable this filter - # or improve current implementation to filter only issues you - # created or assigned or mentioned - # @filter_params[:authorized_only] = true - end - - @filter_params - end - - def get_issues_collection - set_filters_params - @issuable_finder = IssuesFinder.new(current_user, @filter_params) - @issuable_finder.execute - end - - def get_merge_requests_collection - set_filters_params - @issuable_finder = MergeRequestsFinder.new(current_user, @filter_params) - @issuable_finder.execute - end - def import_sources_enabled? !current_application_settings.import_sources.empty? end @@ -363,24 +327,4 @@ class ApplicationController < ActionController::Base def u2f_app_id request.base_url end - - private - - def set_default_sort - key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests') - 'issuable_sort' - end - - cookies[key] = params[:sort] if key && params[:sort].present? - params[:sort] = cookies[key] if key - params[:sort] ||= 'id_desc' - end - - def is_a_listing_page_for?(page_type) - controller_name, action_name = params.values_at(:controller, :action) - - (controller_name == "projects/#{page_type}" && action_name == 'index') || - (controller_name == 'groups' && action_name == page_type) || - (controller_name == 'dashboard' && action_name == page_type) - end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb new file mode 100644 index 00000000000..c802922e0af --- /dev/null +++ b/app/controllers/concerns/issuable_collections.rb @@ -0,0 +1,79 @@ +module IssuableCollections + extend ActiveSupport::Concern + include SortingHelper + + included do + helper_method :issues_finder + helper_method :merge_requests_finder + end + + private + + def issues_collection + issues_finder.execute + end + + def merge_requests_collection + merge_requests_finder.execute + end + + def issues_finder + @issues_finder ||= issuable_finder_for(IssuesFinder) + end + + def merge_requests_finder + @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) + end + + def issuable_finder_for(finder_class) + finder_class.new(current_user, filter_params) + end + + def filter_params + set_sort_order_from_cookie + set_default_scope + set_default_state + + @filter_params = params.dup + @filter_params[:sort] ||= default_sort_order + + @sort = @filter_params[:sort] + + if @project + @filter_params[:project_id] = @project.id + elsif @group + @filter_params[:group_id] = @group.id + else + # TODO: this filter ignore issues/mr created in public or + # internal repos where you are not a member. Enable this filter + # or improve current implementation to filter only issues you + # created or assigned or mentioned + # @filter_params[:authorized_only] = true + end + + @filter_params + end + + def set_default_scope + params[:scope] = 'all' if params[:scope].blank? + end + + def set_default_state + params[:state] = 'opened' if params[:state].blank? + end + + def set_sort_order_from_cookie + key = 'issuable_sort' + + cookies[key] = params[:sort] if params[:sort].present? + params[:sort] = cookies[key] + end + + def default_sort_order + case params[:state] + when 'opened', 'all' then sort_value_recently_created + when 'merged', 'closed' then sort_value_recently_updated + else sort_value_recently_created + end + end +end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 4feabc32b1c..b89fb94be6e 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -1,12 +1,14 @@ module IssuesAction extend ActiveSupport::Concern + include IssuableCollections def issues - @issues = get_issues_collection.non_archived - @issues = @issues.page(params[:page]) - @issues = @issues.preload(:author, :project) + @label = issues_finder.labels.first - @label = @issuable_finder.labels.first + @issues = issues_collection + .non_archived + .preload(:author, :project) + .page(params[:page]) respond_to do |format| format.html diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 06a6b065e7e..a1b0eee37f9 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -1,11 +1,13 @@ module MergeRequestsAction extend ActiveSupport::Concern + include IssuableCollections def merge_requests - @merge_requests = get_merge_requests_collection.non_archived - @merge_requests = @merge_requests.page(params[:page]) - @merge_requests = @merge_requests.preload(:author, :target_project) + @label = merge_requests_finder.labels.first - @label = @issuable_finder.labels.first + @merge_requests = merge_requests_collection + .non_archived + .preload(:author, :target_project) + .page(params[:page]) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3c6f29ac0ba..7f5c3ff3d6a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,6 +3,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleSubscriptionAction include IssuableActions include ToggleAwardEmoji + include IssuableCollections before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, @@ -24,7 +25,7 @@ class Projects::IssuesController < Projects::ApplicationController def index terms = params['issue_search'] - @issues = get_issues_collection + @issues = issues_collection if terms.present? if terms =~ /\A#(\d+)\z/ diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 47c21a18b33..03166294ddd 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -5,6 +5,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include IssuableActions include NotesHelper include ToggleAwardEmoji + include IssuableCollections before_action :module_enabled before_action :merge_request, only: [ @@ -29,7 +30,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index terms = params['issue_search'] - @merge_requests = get_merge_requests_collection + @merge_requests = merge_requests_collection if terms.present? if terms =~ /\A[#!](\d+)\z/ diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index a0932712bd0..33daac0399e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -109,7 +109,7 @@ class IssuableFinder scope.where(title: params[:milestone_title]) else - nil + Milestone.none end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 03495cf5ec4..50de93d4bdf 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -245,7 +245,6 @@ module ApplicationHelper milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], - sort: params[:sort], issue_search: params[:issue_search], label_name: params[:label_name] } diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index d86f1999f5c..e1c0b497550 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -102,11 +102,11 @@ module SortingHelper end def sort_value_oldest_created - 'id_asc' + 'created_asc' end def sort_value_recently_created - 'id_desc' + 'created_desc' end def sort_value_milestone_soon diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index d0edd2f22ec..1a87045aa60 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -19,7 +19,13 @@ Subscribe = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, + @project, + issue: { assignee_id: issues_finder.assignee.try(:id), + milestone_id: issues_finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New Issue", + id: "new_issue_link" do New Issue = render 'shared/issuable/filter', type: :issues diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb new file mode 100644 index 00000000000..0d495cd04aa --- /dev/null +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +describe 'Projects > Issuables > Default sort order', feature: true do + let(:project) { create(:empty_project, :public) } + + let(:first_created_issuable) { issuables.order_created_asc.first } + let(:last_created_issuable) { issuables.order_created_desc.first } + + let(:first_updated_issuable) { issuables.order_updated_asc.first } + let(:last_updated_issuable) { issuables.order_updated_desc.first } + + context 'for merge requests' do + include MergeRequestHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + source_branch: "#{issuable_type}_#{i}", + source_project: project }.merge(ts) + end + + MergeRequest.all + end + + context 'in the "merge requests" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests project + + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / open" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests_with_state(project, 'open') + + expect(selected_sort_order).to eq('last created') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / merged" tab', js: true do + let(:issuable_type) { :merged_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'merged') + + expect(selected_sort_order).to eq('last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / closed" tab', js: true do + let(:issuable_type) { :closed_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'closed') + + expect(selected_sort_order).to eq('last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / all" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests_with_state(project, 'all') + + expect(selected_sort_order).to eq('last created') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + end + + context 'for issues' do + include IssueHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + project: project }.merge(ts) + end + + Issue.all + end + + context 'in the "issues" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues project + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / open" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues_with_state(project, 'open') + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / closed" tab', js: true do + let(:issuable_type) { :closed_issue } + + it 'is "last updated"' do + visit_issues_with_state(project, 'closed') + + expect(selected_sort_order).to eq('last updated') + expect(first_issue).to include(last_updated_issuable.title) + expect(last_issue).to include(first_updated_issuable.title) + end + end + + context 'in the "issues / all" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues_with_state(project, 'all') + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + end + + def selected_sort_order + find('.pull-right .dropdown button').text.downcase + end + + def visit_merge_requests_with_state(project, state) + visit_merge_requests project + visit_issuables_with_state state + end + + def visit_issues_with_state(project, state) + visit_issues project + visit_issuables_with_state state + end + + def visit_issuables_with_state(state) + within('.issues-state-filters') { find("span", text: state.titleize).click } + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 93dcb2ec3fc..9c92b52898c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Issues', feature: true do + include IssueHelpers include SortingHelper let(:project) { create(:project) } @@ -186,15 +187,15 @@ describe 'Issues', feature: true do it 'sorts by newest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) - expect(first_issue).to include('baz') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by oldest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) - expect(first_issue).to include('foo') - expect(last_issue).to include('baz') + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by most recently updated' do @@ -350,8 +351,8 @@ describe 'Issues', feature: true do sort: sort_value_oldest_created, assignee_id: user2.id) - expect(first_issue).to include('foo') - expect(last_issue).to include('bar') + expect(first_issue).to include('bar') + expect(last_issue).to include('foo') expect(page).not_to have_content 'baz' end end @@ -590,14 +591,6 @@ describe 'Issues', feature: true do end end - def first_issue - page.all('ul.issues-list > li').first.text - end - - def last_issue - page.all('ul.issues-list > li').last.text - end - def drop_in_dropzone(file_path) # Generate a fake input selector page.execute_script <<-JS diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index 1c130057c56..cabb8e455f9 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Projects > Merge requests > User lists merge requests', feature: true do + include MergeRequestHelpers include SortingHelper let(:project) { create(:project, :public) } @@ -23,10 +24,12 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true milestone: create(:milestone, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) + # lfs in itself is not a great choice for the title if one wants to match the whole body content later on + # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name create(:merge_request, - title: 'lfs', + title: 'merge_lfs', source_project: project, - source_branch: 'lfs', + source_branch: 'merge_lfs', created_at: 3.minutes.ago, updated_at: 10.seconds.ago) end @@ -35,7 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true visit_merge_requests(project, assignee_id: IssuableFinder::NONE) expect(current_path).to eq(namespace_project_merge_requests_path(project.namespace, project)) - expect(page).to have_content 'lfs' + expect(page).to have_content 'merge_lfs' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' expect(count_merge_requests).to eq(1) @@ -44,7 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true it 'filters on a specific assignee' do visit_merge_requests(project, assignee_id: user.id) - expect(page).not_to have_content 'lfs' + expect(page).not_to have_content 'merge_lfs' expect(page).to have_content 'fix' expect(page).to have_content 'markdown' expect(count_merge_requests).to eq(2) @@ -53,23 +56,23 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true it 'sorts by newest' do visit_merge_requests(project, sort: sort_value_recently_created) - expect(first_merge_request).to include('lfs') - expect(last_merge_request).to include('fix') + expect(first_merge_request).to include('fix') + expect(last_merge_request).to include('merge_lfs') expect(count_merge_requests).to eq(3) end it 'sorts by oldest' do visit_merge_requests(project, sort: sort_value_oldest_created) - expect(first_merge_request).to include('fix') - expect(last_merge_request).to include('lfs') + expect(first_merge_request).to include('merge_lfs') + expect(last_merge_request).to include('fix') expect(count_merge_requests).to eq(3) end it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) - expect(first_merge_request).to include('lfs') + expect(first_merge_request).to include('merge_lfs') expect(count_merge_requests).to eq(3) end @@ -143,18 +146,6 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true end end - def visit_merge_requests(project, opts = {}) - visit namespace_project_merge_requests_path(project.namespace, project, opts) - end - - def first_merge_request - page.all('ul.mr-list > li').first.text - end - - def last_merge_request - page.all('ul.mr-list > li').last.text - end - def count_merge_requests page.all('ul.mr-list > li').count end diff --git a/spec/support/issue_helpers.rb b/spec/support/issue_helpers.rb new file mode 100644 index 00000000000..85241793743 --- /dev/null +++ b/spec/support/issue_helpers.rb @@ -0,0 +1,13 @@ +module IssueHelpers + def visit_issues(project, opts = {}) + visit namespace_project_issues_path project.namespace, project, opts + end + + def first_issue + page.all('ul.issues-list > li').first.text + end + + def last_issue + page.all('ul.issues-list > li').last.text + end +end diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb new file mode 100644 index 00000000000..d5801c8272f --- /dev/null +++ b/spec/support/merge_request_helpers.rb @@ -0,0 +1,13 @@ +module MergeRequestHelpers + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path project.namespace, project, opts + end + + def first_merge_request + page.all('ul.mr-list > li').first.text + end + + def last_merge_request + page.all('ul.mr-list > li').last.text + end +end From 44eec823fb68238705eb932dd14aa211b730a316 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Mon, 1 Aug 2016 12:29:40 +0200 Subject: [PATCH 141/198] Avoid line_code and position calculation on line partial for plain view --- CHANGELOG | 1 + app/views/projects/diffs/_line.html.haml | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9075972e6d0..7eef5996031 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,7 @@ v 8.11.0 (unreleased) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed + - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab. - Add commit stats in commit api. !5517 (dixpac) - Make error pages responsive (Takuya Noguchi) - Change requests_profiles resource constraint to catch virtually any file diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 5a8a131d10c..4d3af905b58 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,8 +1,7 @@ - plain = local_assigns.fetch(:plain, false) -- line_code = diff_file.line_code(line) -- position = diff_file.position(line) - type = line.type -%tr.line_holder{ id: line_code, class: type } +- line_code = diff_file.line_code(line) unless plain +%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } } - case type - when 'match' = render "projects/diffs/match_line", { line: line.text, @@ -24,4 +23,4 @@ = link_text - else %a{href: "##{line_code}", data: { linenumber: link_text }} - %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, position, type) unless plain) }= diff_line_content(line.text, type) + %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }= diff_line_content(line.text, type) From 3fe18525ddca414017d330e992999bad05002fa8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 31 Jul 2016 20:51:12 -0700 Subject: [PATCH 142/198] Trim extra displayed carriage returns in diffs and files with CRLFs Closes #20440 --- CHANGELOG | 1 + lib/rouge/formatters/html_gitlab.rb | 2 +- spec/lib/gitlab/highlight_spec.rb | 12 ++++++++++++ spec/support/test_env.rb | 3 ++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 95aeda301f7..0061bba03b9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 8.10.3 (unreleased) - Properly abort a merge when merge conflicts occur - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. - Fix Import/Export issue importing milestones and labels not associated properly + - Trim extra displayed carriage returns in diffs and files with CRLFs v 8.10.2 - User can now search branches by name. !5144 diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index f818dc78d34..4edfd015074 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -18,7 +18,7 @@ module Rouge is_first = false yield %() - line.each { |token, value| yield span(token, value) } + line.each { |token, value| yield span(token, value.chomp) } yield %() @line_number += 1 diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 364532e94e3..80a9473d6aa 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -17,6 +17,18 @@ describe Gitlab::Highlight, lib: true do expect(lines[21]).to eq(%Q{ unless File.directory?(path)\n}) expect(lines[26]).to eq(%Q{ @cmd_status = 0\n}) end + + describe 'with CRLF' do + let(:branch) { 'crlf-diff' } + let(:blob) { repository.blob_at_branch(branch, path) } + let(:lines) do + Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff-test', 'files/whitespace') + end + + it 'strips extra LFs' do + expect(lines[0]).to eq("test ") + end + end end describe 'custom highlighting from .gitattributes' do diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 3735abe2302..4561aa9644d 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -21,7 +21,8 @@ module TestEnv 'expand-collapse-diffs' => '4842455', 'expand-collapse-files' => '025db92', 'expand-collapse-lines' => '238e82d', - 'video' => '8879059' + 'video' => '8879059', + 'crlf-diff' => '5938907' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily From fe25d1d5cfdd8c52854b459b49bbead1a608822c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 1 Aug 2016 13:16:04 +0200 Subject: [PATCH 143/198] Fix specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/finders/branches_finder_spec.rb | 2 +- spec/lib/gitlab/highlight_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 6ea9a3a3ec5..482caeee64a 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -20,7 +20,7 @@ describe BranchesFinder do result = branches_finder.execute - expect(result.first.name).to eq('video') + expect(result.first.name).to eq('crlf-diff') end it 'sorts by last_updated' do diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 80a9473d6aa..fc021416d92 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::Highlight, lib: true do let(:branch) { 'crlf-diff' } let(:blob) { repository.blob_at_branch(branch, path) } let(:lines) do - Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff-test', 'files/whitespace') + Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace') end it 'strips extra LFs' do From 2dcfaa19833256f7638153a499ed907f86949dd6 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Mon, 1 Aug 2016 14:08:43 +0200 Subject: [PATCH 144/198] Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration --- CHANGELOG | 1 + lib/gitlab/metrics.rb | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c9658e4ba5c..f3245f8799f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -30,6 +30,7 @@ v 8.11.0 (unreleased) - Make branches sortable without push permission !5462 (winniehell) - Check for Ci::Build artifacts at database level on pipeline partial - Make "New issue" button in Issue page less obtrusive !5457 (winniehell) + - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 081576a440c..41fcd971c22 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -141,10 +141,9 @@ module Gitlab end end + # Allow access from other metrics related middlewares def self.current_transaction Transaction.current end - - private_class_method :current_transaction end end From 2e06800bfdb1fa46602beffce6ea8fb282e8a29a Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Mon, 1 Aug 2016 14:23:41 +0200 Subject: [PATCH 145/198] Fix RequestProfiler::Middleware error when code is reloaded in development Closes #20452 --- CHANGELOG | 1 + config/initializers/request_profiler.rb | 2 ++ lib/gitlab/request_profiler/middleware.rb | 1 + 3 files changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 387006eec4e..d25ff7da27f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -40,6 +40,7 @@ v 8.11.0 (unreleased) - Change requests_profiles resource constraint to catch virtually any file - Reduce number of queries made for merge_requests/:id/diffs - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) + - Fix RequestProfiler::Middleware error when code is reloaded in development v 8.10.3 (unreleased) - Fix hooks missing on imported GitLab projects diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb index fb5a7b8372e..a9aa802681a 100644 --- a/config/initializers/request_profiler.rb +++ b/config/initializers/request_profiler.rb @@ -1,3 +1,5 @@ +require 'gitlab/request_profiler/middleware' + Rails.application.configure do |config| config.middleware.use(Gitlab::RequestProfiler::Middleware) end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 8da8b754975..0c54f2dd71f 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -1,4 +1,5 @@ require 'ruby-prof' +require 'gitlab/request_profiler' module Gitlab module RequestProfiler From 7e77b1fd39f60d5b311bdaa350acbf005a4b398e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 1 Aug 2016 12:45:27 +0000 Subject: [PATCH 146/198] Update CHANGELOG --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4095eaa6e63..9075972e6d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,7 +41,6 @@ v 8.10.3 (unreleased) - Fix hooks missing on imported GitLab projects - Properly abort a merge when merge conflicts occur - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. - - Fix timing problems running imports on production v 8.10.2 - User can now search branches by name. !5144 From 8bcdc4b185d70bfb2a50d22ec99a26c2c463a2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 1 Aug 2016 13:52:51 +0000 Subject: [PATCH 147/198] API methods should be documented using Grape's DSL See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2397#note_13491048 --- doc/development/doc_styleguide.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 6ee7b3cfeeb..3a3597bccaa 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -244,6 +244,12 @@ In this case: Here is a list of must-have items. Use them in the exact order that appears on this document. Further explanation is given below. +- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods) + (see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb + for a good example): + - `desc` for the method summary (you can pass it a block for additional details) + - `params` for the method params (this acts as description **and** validation + of the params) - Every method must have the REST API request. For example: ``` From 9845079950da010b8a4c07777f984aaf02642ad0 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 29 Jul 2016 16:20:00 -0300 Subject: [PATCH 148/198] Fix search results for notes without commits --- CHANGELOG | 1 + app/views/search/results/_note.html.haml | 8 ++++++-- spec/features/search_spec.rb | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 39b77460deb..e46befcec2a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 8.11.0 (unreleased) - Check for Ci::Build artifacts at database level on pipeline partial - Make "New issue" button in Issue page less obtrusive !5457 (winniehell) - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration + - Fix search for notes which belongs to deleted objects - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 8163aff43b6..e0400083870 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,6 +1,7 @@ - project = note.project - note_url = Gitlab::UrlBuilder.build(note) -- noteable_identifier = note.noteable.try(:iid) || note.noteable.id +- noteable_identifier = note.noteable.try(:iid) || note.noteable.try(:id) + .search-result-row %h5.note-search-caption.str-truncated %i.fa.fa-comment @@ -10,7 +11,10 @@ · - if note.for_commit? - = link_to "Commit #{truncate_sha(note.commit_id)}", note_url + = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do + = truncate_sha(note.commit_id) + %span.light Commit deleted + - else %span #{note.noteable_type.titleize} ##{noteable_identifier} · diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index d0a301038c4..09f70cd3b00 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -28,6 +28,26 @@ describe "Search", feature: true do end context 'search for comments' do + context 'when comment belongs to a invalid commit' do + let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') } + + before { note.update_attributes(commit_id: 12345678) } + + it 'finds comment' do + visit namespace_project_path(project.namespace, project) + + page.within '.search' do + fill_in 'search', with: note.note + click_button 'Go' + end + + click_link 'Comments' + + expect(page).to have_text("Commit deleted") + expect(page).to have_text("12345678") + end + end + it 'finds a snippet' do snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title') note = create(:note, From af7ce322bdbaf74eaf54eac92c2ed5183e0d8e9c Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Wed, 27 Jul 2016 15:39:45 -0400 Subject: [PATCH 149/198] webhooks: include old revision in MR update events --- CHANGELOG | 1 + app/services/merge_requests/base_service.rb | 9 ++++++--- app/services/merge_requests/refresh_service.rb | 2 +- spec/services/merge_requests/refresh_service_spec.rb | 6 +++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 39b77460deb..4285a548e64 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.11.0 (unreleased) - Optimize checking if a user has read access to a list of issues !5370 - Nokogiri's various parsing methods are now instrumented - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 + - Include old revision in merge request update hooks (Ben Boeckel) - Add build event color in HipChat messages (David Eisner) - Make fork counter always clickable. !5463 (winniehell) - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333 diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index bc3606a14c2..ba424b09463 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -17,16 +17,19 @@ module MergeRequests end end - def hook_data(merge_request, action) + def hook_data(merge_request, action, oldrev = nil) hook_data = merge_request.to_hook_data(current_user) hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) hook_data[:object_attributes][:action] = action + if oldrev && !Gitlab::Git.blank_ref?(oldrev) + hook_data[:object_attributes][:oldrev] = oldrev + end hook_data end - def execute_hooks(merge_request, action = 'open') + def execute_hooks(merge_request, action = 'open', oldrev = nil) if merge_request.project - merge_data = hook_data(merge_request, action) + merge_data = hook_data(merge_request, action, oldrev) merge_request.project.execute_hooks(merge_data, :merge_request_hooks) merge_request.project.execute_services(merge_data, :merge_request_hooks) end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 1daf6bbf553..5cedd6f11d9 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -137,7 +137,7 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks merge_requests_for_source_branch.each do |merge_request| - execute_hooks(merge_request, 'update') + execute_hooks(merge_request, 'update', @oldrev) end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index ce643b3f860..781ee7ffed3 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -57,7 +57,7 @@ describe MergeRequests::RefreshService, services: true do it 'should execute hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update') + with(@merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).not_to be_empty } @@ -113,7 +113,7 @@ describe MergeRequests::RefreshService, services: true do it 'should execute hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).to be_empty } @@ -158,7 +158,7 @@ describe MergeRequests::RefreshService, services: true do it 'refreshes the merge request' do expect(refresh_service).to receive(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev) refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') From d61b92a350f50fa1a443f0a180da401684f5cdca Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 1 Aug 2016 12:20:38 -0500 Subject: [PATCH 150/198] Convert image diff background image to CSS --- CHANGELOG | 1 + app/assets/images/trans_bg.gif | Bin 49 -> 0 bytes app/assets/stylesheets/pages/diff.scss | 5 ++++- 3 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 app/assets/images/trans_bg.gif diff --git a/CHANGELOG b/CHANGELOG index 39b77460deb..71e837ad291 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,7 @@ v 8.11.0 (unreleased) - Bump gitlab_git to speedup DiffCollection iterations - Make branches sortable without push permission !5462 (winniehell) - Check for Ci::Build artifacts at database level on pipeline partial + - Convert image diff background image to CSS (ClemMakesApps) - Make "New issue" button in Issue page less obtrusive !5457 (winniehell) - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) diff --git a/app/assets/images/trans_bg.gif b/app/assets/images/trans_bg.gif deleted file mode 100644 index 1a1c9c15ec71a58db869578399068cf313c51599..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49 zcmZ?wbh9u| Date: Mon, 1 Aug 2016 15:09:03 -0500 Subject: [PATCH 151/198] Convert switch icon into icon font --- CHANGELOG | 1 + app/assets/images/switch_icon.png | Bin 231 -> 0 bytes app/assets/stylesheets/pages/commits.scss | 2 -- app/views/projects/compare/_form.html.haml | 2 +- 4 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 app/assets/images/switch_icon.png diff --git a/CHANGELOG b/CHANGELOG index 39b77460deb..e7eec5f3948 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.11.0 (unreleased) - Fix the title of the toggle dropdown button. !5515 (herminiotorres) - Improve diff performance by eliminating redundant checks for text blobs + - Convert switch icon into icon font (ClemMakesApps) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Fix CI status icon link underline (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive diff --git a/app/assets/images/switch_icon.png b/app/assets/images/switch_icon.png deleted file mode 100644 index c6b6c8d9521f64b00990ca5352c8ce269e9a3e4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRp!2%?I7)^T!q}F-5IEGX(CQB$JyiPgI(CpB{ znQLZw!_vS(fw?E~cIIX=wzc*<{+FNFBDg7WLn4Ec#-4Yd?#uGk{`(zUR92o=kj9|I zu|@o=*mvRPL*8}TeFk_qI4H=%Le3NA&A1pgl_%;P@lenLx}3q&)z4*}Q$iB}^Pp6) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0298577c494..2beef15bbf9 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,8 +1,6 @@ .commits-compare-switch { @include btn-default; @include btn-white; - background: image-url("switch_icon.png") no-repeat center center; - text-indent: -9999px; float: left; margin-right: 9px; } diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index af09b3418ea..d79336f5a60 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,7 +1,7 @@ = form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do .clearfix - if params[:to] && params[:from] - = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} + = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} .form-group.dropdown.compare-form-group.js-compare-from-dropdown .input-group.inline-input-group %span.input-group-addon from From 0720b9ce0059feca284404e6fc1ede0cba542fe3 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Mon, 1 Aug 2016 16:55:50 +0200 Subject: [PATCH 152/198] Catch what warden might throw when profiling requests to re-throw it Closes #20488 --- CHANGELOG | 1 + lib/gitlab/request_profiler/middleware.rb | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3b61e52b2fc..0cff6857c2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 8.11.0 (unreleased) - Reduce number of queries made for merge_requests/:id/diffs - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) - Fix RequestProfiler::Middleware error when code is reloaded in development + - Catch what warden might throw when profiling requests to re-throw it v 8.10.3 (unreleased) - Fix importer for GitHub Pull Requests when a branch was removed diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 0c54f2dd71f..4e787dc0656 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -29,7 +29,9 @@ module Gitlab def call_with_profiling(env) ret = nil result = RubyProf::Profile.profile do - ret = @app.call(env) + ret = catch(:warden) do + @app.call(env) + end end printer = RubyProf::CallStackPrinter.new(result) @@ -41,7 +43,11 @@ module Gitlab printer.print(file) end - ret + if ret.is_a?(Array) + ret + else + throw(:warden, ret) + end end end end From 99c02ed53c994fbd71442410c78daf220c6d1ced Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 1 Aug 2016 13:11:45 -0700 Subject: [PATCH 153/198] Only use RequestStore in ProjectTeam#max_member_access_for_user if it is active --- app/models/project_team.rb | 9 +++- spec/models/project_team_spec.rb | 91 ++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index fdfaf052730..19fd082534c 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -138,8 +138,13 @@ class ProjectTeam def max_member_access_for_user_ids(user_ids) user_ids = user_ids.uniq key = "max_member_access:#{project.id}" - RequestStore.store[key] ||= {} - access = RequestStore.store[key] + + access = {} + + if RequestStore.active? + RequestStore.store[key] ||= {} + access = RequestStore.store[key] + end # Lookup only the IDs we need user_ids = user_ids - access.keys diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 1f42fbd3385..5eaf0d3b7a6 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -199,48 +199,69 @@ describe ProjectTeam, models: true do end end - describe "#max_member_access_for_users" do - it 'returns correct roles for different users' do - master = create(:user) - reporter = create(:user) - promoted_guest = create(:user) - guest = create(:user) - project = create(:project) + shared_examples_for "#max_member_access_for_users" do |enable_request_store| + describe "#max_member_access_for_users" do + before do + RequestStore.begin! if enable_request_store + end - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [promoted_guest, :guest] - project.team << [guest, :guest] + after do + if enable_request_store + RequestStore.end! + RequestStore.clear! + end + end - group = create(:group) - group_developer = create(:user) - second_developer = create(:user) - project.project_group_links.create( - group: group, - group_access: Gitlab::Access::DEVELOPER) + it 'returns correct roles for different users' do + master = create(:user) + reporter = create(:user) + promoted_guest = create(:user) + guest = create(:user) + project = create(:project) - group.add_master(promoted_guest) - group.add_developer(group_developer) - group.add_developer(second_developer) + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [promoted_guest, :guest] + project.team << [guest, :guest] - second_group = create(:group) - project.project_group_links.create( - group: second_group, - group_access: Gitlab::Access::MASTER) - second_group.add_master(second_developer) + group = create(:group) + group_developer = create(:user) + second_developer = create(:user) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER) - users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) + group.add_master(promoted_guest) + group.add_developer(group_developer) + group.add_developer(second_developer) - expected = { - master.id => Gitlab::Access::MASTER, - reporter.id => Gitlab::Access::REPORTER, - promoted_guest.id => Gitlab::Access::DEVELOPER, - guest.id => Gitlab::Access::GUEST, - group_developer.id => Gitlab::Access::DEVELOPER, - second_developer.id => Gitlab::Access::MASTER - } + second_group = create(:group) + project.project_group_links.create( + group: second_group, + group_access: Gitlab::Access::MASTER) + second_group.add_master(second_developer) - expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) + users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) + + expected = { + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER, + promoted_guest.id => Gitlab::Access::DEVELOPER, + guest.id => Gitlab::Access::GUEST, + group_developer.id => Gitlab::Access::DEVELOPER, + second_developer.id => Gitlab::Access::MASTER + } + + expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) + end end end + + describe '#max_member_access_for_users with RequestStore' do + it_behaves_like "#max_member_access_for_users", true + end + + describe '#max_member_access_for_users without RequestStore' do + it_behaves_like "#max_member_access_for_users", false + end end From 957331bf45e33c5d1ca0331ca6acb56fc8ecdb92 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 1 Aug 2016 15:14:47 -0700 Subject: [PATCH 154/198] Update CHANGELOG for 8.10.3 [ci skip] --- CHANGELOG | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0cff6857c2c..4daf9cd9092 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,13 +46,15 @@ v 8.11.0 (unreleased) - Fix RequestProfiler::Middleware error when code is reloaded in development - Catch what warden might throw when profiling requests to re-throw it -v 8.10.3 (unreleased) - - Fix importer for GitHub Pull Requests when a branch was removed - - Fix hooks missing on imported GitLab projects - - Properly abort a merge when merge conflicts occur - - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. - - Fix Import/Export issue importing milestones and labels not associated properly - - Trim extra displayed carriage returns in diffs and files with CRLFs +v 8.10.3 + - Fix Import/Export issue importing milestones and labels not associated properly. !5426 + - Fix timing problems running imports on production. !5523 + - Add a log message when a project is scheduled for destruction for debugging. !5540 + - Fix hooks missing on imported GitLab projects. !5549 + - Properly abort a merge when merge conflicts occur. !5569 + - Fix importer for GitHub Pull Requests when a branch was removed. !5573 + - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584 + - Trim extra displayed carriage returns in diffs and files with CRLFs. !5588 v 8.10.2 - User can now search branches by name. !5144 From 6cee51903dc39b8071594d83ac0703ccfe4388fd Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Mon, 1 Aug 2016 18:38:41 -0500 Subject: [PATCH 155/198] Update installation guide for 8.11 --- doc/install/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 9bc0dbb5e2a..af8e31a705b 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -269,9 +269,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-10-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-11-stable gitlab -**Note:** You can change `8-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It From 3f2f4bda456a6e04aca6ab27983b337ddfdef3c5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 31 Jul 2016 18:37:15 -0700 Subject: [PATCH 156/198] Remove delay when hitting Reply... button on page with a lot of discussions --- app/assets/javascripts/gfm_auto_complete.js | 4 ++-- app/assets/javascripts/gl_form.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 41f4c1914f2..2e5b15f4b77 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -47,8 +47,8 @@ } } }, - setup: function(wrap) { - this.input = $('.js-gfm-input'); + setup: function(input) { + this.input = input || $('.js-gfm-input'); this.destroyAtWho(); this.setupAtWho(); if (this.dataSource) { diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 6ac7564a848..528a673eb15 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -21,7 +21,7 @@ this.form.find('.div-dropzone').remove(); this.form.addClass('gfm-form'); disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - GitLab.GfmAutoComplete.setup(); + GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new DropzoneInput(this.form); autosize(this.textarea); this.addEventListeners(); From 6e87ce282db9f5264f77ae2238c1fff23beece1a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 1 Aug 2016 16:21:37 -0700 Subject: [PATCH 157/198] Add changelog item --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 4daf9cd9092..d6f928fe225 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.11.0 (unreleased) - Clean up unused routes (Josef Strzibny) - Add green outline to New Branch button. !5447 (winniehell) - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects + - Remove delay when hitting "Reply..." button on page with a lot of discussions - Retrieve rendered HTML from cache in one request - Fix renaming repository when name contains invalid chararacters under project settings - Optimize checking if a user has read access to a list of issues !5370 From a70431f874112212cb44b7a104b2e32f440af941 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 1 Aug 2016 16:59:44 -0700 Subject: [PATCH 158/198] Redirect to external issue tracker from `/issues` Prior, in order to display the correct link to "Issues" in the project navigation, we were performing a check against the project to see if it used an external issue tracker, and if so, we used that URL. This was inefficient. Now, we simply _always_ link to `namespace_project_issues_path`, and then in the controller we redirect to the external tracker if it's present. This also removes the need for the url_for_issue helper. Bonus! :tada: --- app/controllers/projects/issues_controller.rb | 7 +++ app/helpers/issues_helper.rb | 16 ----- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/issues/_head.html.haml | 2 +- .../projects/issues_controller_spec.rb | 58 ++++++++++++------- spec/helpers/issues_helper_spec.rb | 46 --------------- 6 files changed, 45 insertions(+), 86 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7f5c3ff3d6a..cb1e514c60e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -5,6 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleAwardEmoji include IssuableCollections + before_action :redirect_to_external_issue_tracker, only: [:index] before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, :related_branches, :can_create_branch] @@ -201,6 +202,12 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless @project.issues_enabled && @project.default_issues_tracker? end + def redirect_to_external_issue_tracker + return unless @project.external_issue_tracker + + redirect_to @project.external_issue_tracker.issues_url + end + # Since iids are implemented only in 6.1 # user may navigate to issue page using old global ids. # diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 2b0defd1dda..5061ccb93a4 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -13,22 +13,6 @@ module IssuesHelper OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') end - def url_for_project_issues(project = @project, options = {}) - return '' if project.nil? - - url = - if options[:only_path] - project.issues_tracker.project_path - else - project.issues_tracker.project_url - end - - # Ensure we return a valid URL to prevent possible XSS. - URI.parse(url).to_s - rescue URI::InvalidURIError - '' - end - def url_for_new_issue(project = @project, options = {}) return '' if project.nil? diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 9e65d94186b..1d3b8fc3683 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -66,7 +66,7 @@ - if project_nav_tab? :issues = nav_link(controller: [:issues, :labels, :milestones]) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do %span Issues - if @project.default_issues_tracker? diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 403adb7426b..60b45115b73 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -2,7 +2,7 @@ %ul{ class: (container_class) } - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do %span Issues diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 77f65057f71..ed31f689d3d 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -6,37 +6,51 @@ describe Projects::IssuesController do let(:issue) { create(:issue, project: project) } describe "GET #index" do - before do - sign_in(user) - project.team << [user, :developer] + context 'external issue tracker' do + it 'redirects to the external issue tracker' do + external = double(issues_url: 'https://example.com/issues') + allow(project).to receive(:external_issue_tracker).and_return(external) + controller.instance_variable_set(:@project, project) + + get :index, namespace_id: project.namespace.path, project_id: project + + expect(response).to redirect_to('https://example.com/issues') + end end - it "returns index" do - get :index, namespace_id: project.namespace.path, project_id: project.path + context 'internal issue tracker' do + before do + sign_in(user) + project.team << [user, :developer] + end - expect(response).to have_http_status(200) - end + it "returns index" do + get :index, namespace_id: project.namespace.path, project_id: project.path - it "return 301 if request path doesn't match project path" do - get :index, namespace_id: project.namespace.path, project_id: project.path.upcase + expect(response).to have_http_status(200) + end - expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) - end + it "return 301 if request path doesn't match project path" do + get :index, namespace_id: project.namespace.path, project_id: project.path.upcase - it "returns 404 when issues are disabled" do - project.issues_enabled = false - project.save + expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) + end - get :index, namespace_id: project.namespace.path, project_id: project.path - expect(response).to have_http_status(404) - end + it "returns 404 when issues are disabled" do + project.issues_enabled = false + project.save - it "returns 404 when external issue tracker is enabled" do - controller.instance_variable_set(:@project, project) - allow(project).to receive(:default_issues_tracker?).and_return(false) + get :index, namespace_id: project.namespace.path, project_id: project.path + expect(response).to have_http_status(404) + end - get :index, namespace_id: project.namespace.path, project_id: project.path - expect(response).to have_http_status(404) + it "returns 404 when external issue tracker is enabled" do + controller.instance_variable_set(:@project, project) + allow(project).to receive(:default_issues_tracker?).and_return(false) + + get :index, namespace_id: project.namespace.path, project_id: project.path + expect(response).to have_http_status(404) + end end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 831ae7fb69c..ca4aea47413 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -5,52 +5,6 @@ describe IssuesHelper do let(:issue) { create :issue, project: project } let(:ext_project) { create :redmine_project } - describe "url_for_project_issues" do - let(:project_url) { ext_project.external_issue_tracker.project_url } - let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) } - let(:int_expected) { polymorphic_path([@project.namespace, project]) } - - it "should return internal path if used internal tracker" do - @project = project - expect(url_for_project_issues).to match(int_expected) - end - - it "should return path to external tracker" do - @project = ext_project - - expect(url_for_project_issues).to match(ext_expected) - end - - it "should return empty string if project nil" do - @project = nil - - expect(url_for_project_issues).to eq "" - end - - it 'returns an empty string if project_url is invalid' do - expect(project).to receive_message_chain('issues_tracker.project_url') { 'javascript:alert("foo");' } - - expect(url_for_project_issues(project)).to eq '' - end - - it 'returns an empty string if project_path is invalid' do - expect(project).to receive_message_chain('issues_tracker.project_path') { 'javascript:alert("foo");' } - - expect(url_for_project_issues(project, only_path: true)).to eq '' - end - - describe "when external tracker was enabled and then config removed" do - before do - @project = ext_project - allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) - end - - it "should return path to external tracker" do - expect(url_for_project_issues).to match(ext_expected) - end - end - end - describe "url_for_issue" do let(:issues_url) { ext_project.external_issue_tracker.issues_url} let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) } From 901d4d2ca54d173f9c6b1f39c7548ef7fc9e8cd7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 1 Aug 2016 18:23:12 -0700 Subject: [PATCH 159/198] Remove `url_for_new_issue` helper Now we link to the standard `IssuesController#new` action, and let it redirect if we're using an external tracker. --- app/controllers/projects/issues_controller.rb | 12 +++-- app/helpers/issues_helper.rb | 16 ------- .../projects/buttons/_dropdown.html.haml | 2 +- .../projects/issues_controller_spec.rb | 14 ++++++ spec/helpers/issues_helper_spec.rb | 46 ------------------- 5 files changed, 24 insertions(+), 66 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index cb1e514c60e..660e0eba06f 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -5,7 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleAwardEmoji include IssuableCollections - before_action :redirect_to_external_issue_tracker, only: [:index] + before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, :related_branches, :can_create_branch] @@ -203,9 +203,15 @@ class Projects::IssuesController < Projects::ApplicationController end def redirect_to_external_issue_tracker - return unless @project.external_issue_tracker + external = @project.external_issue_tracker - redirect_to @project.external_issue_tracker.issues_url + return unless external + + if action_name == 'new' + redirect_to external.new_issue_path + else + redirect_to external.issues_url + end end # Since iids are implemented only in 6.1 diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 5061ccb93a4..2e82b44437b 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -13,22 +13,6 @@ module IssuesHelper OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') end - def url_for_new_issue(project = @project, options = {}) - return '' if project.nil? - - url = - if options[:only_path] - project.issues_tracker.new_issue_path - else - project.issues_tracker.new_issue_url - end - - # Ensure we return a valid URL to prevent possible XSS. - URI.parse(url).to_s - rescue URI::InvalidURIError - '' - end - def url_for_issue(issue_iid, project = @project, options = {}) return '' if project.nil? diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 16b8e1cca91..ca907077c2b 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -9,7 +9,7 @@ - if can_create_issue %li - = link_to url_for_new_issue(@project, only_path: true) do + = link_to new_namespace_project_issue_path(@project.namespace, @project) do = icon('exclamation-circle fw') New issue diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index ed31f689d3d..ec820de3d09 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -54,6 +54,20 @@ describe Projects::IssuesController do end end + describe 'GET #new' do + context 'external issue tracker' do + it 'redirects to the external issue tracker' do + external = double(new_issue_path: 'https://example.com/issues/new') + allow(project).to receive(:external_issue_tracker).and_return(external) + controller.instance_variable_set(:@project, project) + + get :new, namespace_id: project.namespace.path, project_id: project + + expect(response).to redirect_to('https://example.com/issues/new') + end + end + end + describe 'PUT #update' do context 'when moving issue to another private project' do let(:another_project) { create(:project, :private) } diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index ca4aea47413..9ee46dd2508 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -51,52 +51,6 @@ describe IssuesHelper do end end - describe 'url_for_new_issue' do - let(:issues_url) { ext_project.external_issue_tracker.new_issue_url } - let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) } - let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) } - - it "should return internal path if used internal tracker" do - @project = project - expect(url_for_new_issue).to match(int_expected) - end - - it "should return path to external tracker" do - @project = ext_project - - expect(url_for_new_issue).to match(ext_expected) - end - - it "should return empty string if project nil" do - @project = nil - - expect(url_for_new_issue).to eq "" - end - - it 'returns an empty string if issue_url is invalid' do - expect(project).to receive_message_chain('issues_tracker.new_issue_url') { 'javascript:alert("foo");' } - - expect(url_for_new_issue(project)).to eq '' - end - - it 'returns an empty string if issue_path is invalid' do - expect(project).to receive_message_chain('issues_tracker.new_issue_path') { 'javascript:alert("foo");' } - - expect(url_for_new_issue(project, only_path: true)).to eq '' - end - - describe "when external tracker was enabled and then config removed" do - before do - @project = ext_project - allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) - end - - it "should return internal path" do - expect(url_for_new_issue).to match(ext_expected) - end - end - end - describe "merge_requests_sentence" do subject { merge_requests_sentence(merge_requests)} let(:merge_requests) do From fa7217f3c59cfcc5815524b0415e881e1f9eda62 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Tue, 2 Aug 2016 01:30:35 +0000 Subject: [PATCH 160/198] fix runner install link --- doc/ci/quick_start/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 7fa1a478f34..6a3c416d995 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -233,7 +233,7 @@ Awesome! You started using CI in GitLab! Visit the [examples README][examples] to see a list of examples using GitLab CI with various languages. -[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation +[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ [examples]: ../examples/README.md [ci]: https://about.gitlab.com/gitlab-ci/ From ae9f0ca818b203df3bf61e5598c4e7e63c4c2d70 Mon Sep 17 00:00:00 2001 From: winniehell Date: Sun, 31 Jul 2016 04:09:19 +0200 Subject: [PATCH 161/198] Add failing test for #20462 --- spec/finders/branches_finder_spec.rb | 6 ++- spec/routing/project_routing_spec.rb | 5 ++- spec/support/test_env.rb | 1 + .../projects/tree/show.html.haml_spec.rb | 37 +++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 spec/views/projects/tree/show.html.haml_spec.rb diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 482caeee64a..6fce11de30f 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -20,7 +20,11 @@ describe BranchesFinder do result = branches_finder.execute - expect(result.first.name).to eq('crlf-diff') + recently_updated_branch = repository.branches.max do |a, b| + repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + end + + expect(result.first.name).to eq(recently_updated_branch.name) end it 'sorts by last_updated' do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 9151cd3aefe..b941e78f983 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -479,13 +479,16 @@ end describe Projects::NetworkController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') - expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') + expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') end end describe Projects::GraphsController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') + expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 4561aa9644d..1c0c66969e3 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -6,6 +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', 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb new file mode 100644 index 00000000000..0f3fc1ee1ac --- /dev/null +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'projects/tree/show' do + include Devise::TestHelpers + + let(:project) { create(:project) } + let(:repository) { project.repository } + + before do + assign(:project, project) + assign(:repository, repository) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:can_collaborate_with_project?).and_return(true) + end + + context 'for branch names ending on .json' do + let(:ref) { 'ends-with.json' } + let(:commit) { repository.commit(ref) } + let(:path) { '' } + let(:tree) { repository.tree(commit.id, path) } + + before do + assign(:ref, ref) + assign(:commit, commit) + assign(:id, commit.id) + assign(:tree, tree) + assign(:path, path) + end + + it 'displays correctly' do + render + expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref) + expect(rendered).to have_css('.readme-holder .file-content', text: ref) + end + end +end From 010477edc034330dfe4bce7b4dfac252e1fb0a25 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 7 Jul 2016 12:41:57 +0100 Subject: [PATCH 162/198] Append .json onto graph request URL (!5136) --- app/views/projects/graphs/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index a985b442b2d..8777e0d8fcd 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -32,7 +32,7 @@ :javascript $.ajax({ type: "GET", - url: location.href, + url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, :json)}", dataType: "json", success: function (data) { var graph = new ContributorsStatGraph(); From e1832914df2eccea1730586b26e759b562e8b7c1 Mon Sep 17 00:00:00 2001 From: winniehell Date: Sun, 31 Jul 2016 22:52:44 +0200 Subject: [PATCH 163/198] Allow branch names ending with .json for graph and network page (!5579) --- CHANGELOG | 1 + app/views/projects/graphs/show.html.haml | 2 +- config/routes.rb | 16 ++++++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4daf9cd9092..6bd6f40975f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,6 +34,7 @@ v 8.11.0 (unreleased) - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration - Fix search for notes which belongs to deleted objects - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) + - Allow branch names ending with .json for graph and network page !5579 (winniehell) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 8777e0d8fcd..ac5f792d140 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -32,7 +32,7 @@ :javascript $.ajax({ type: "GET", - url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, :json)}", + url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, format: :json)}", dataType: "json", success: function (data) { var graph = new ContributorsStatGraph(); diff --git a/config/routes.rb b/config/routes.rb index 371eb4bee7f..2f5f32d9e30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -626,13 +626,17 @@ Rails.application.routes.draw do get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ } - resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } + # Don't use format parameter as file extension (old 3.0.x behavior) + # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments + scope format: false do + resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do - member do - get :commits - get :ci - get :languages + resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do + member do + get :commits + get :ci + get :languages + end end end From 701e5ccbe6018129130ee70a78f213c406c93fcf Mon Sep 17 00:00:00 2001 From: winniehell Date: Mon, 1 Aug 2016 03:58:31 +0200 Subject: [PATCH 164/198] Add failing tests for #19028 --- spec/lib/banzai/filter/relative_link_filter_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 9921171f2aa..224baca8030 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -78,12 +78,24 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do end context 'with a valid repository' do + it 'rebuilds absolute URL for a file in the repo' do + doc = filter(link('/doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + it 'rebuilds relative URL for a file in the repo' do doc = filter(link('doc/api/README.md')) expect(doc.at_css('a')['href']). to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end + it 'rebuilds relative URL for a file in the repo with leading ./' do + doc = filter(link('./doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + it 'rebuilds relative URL for a file in the repo up one directory' do relative_link = link('../api/README.md') doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') From 40da543f2fddefcdebf12e52425314355a16a57d Mon Sep 17 00:00:00 2001 From: winniehell Date: Mon, 1 Aug 2016 04:52:05 +0200 Subject: [PATCH 165/198] Add support for relative links starting with ./ or / to RelativeLinkFilter (!5586) --- CHANGELOG | 1 + lib/banzai/filter/relative_link_filter.rb | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4daf9cd9092..c099c63ce86 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.11.0 (unreleased) - Fix the title of the toggle dropdown button. !5515 (herminiotorres) - Improve diff performance by eliminating redundant checks for text blobs - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) + - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Fix CI status icon link underline (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 337fb50317d..5b73fc8fcee 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -87,10 +87,13 @@ module Banzai def build_relative_path(path, request_path) return request_path if path.empty? return path unless request_path + return path[1..-1] if path.start_with?('/') parts = request_path.split('/') parts.pop if uri_type(request_path) != :tree + path.sub!(%r{^\./}, '') + while path.start_with?('../') parts.pop path.sub!('../', '') From f15fb92209ccea3e13916d2f2d3899008f9df578 Mon Sep 17 00:00:00 2001 From: winniehell Date: Tue, 2 Aug 2016 04:50:47 +0200 Subject: [PATCH 166/198] Revert "md5 and utf_encode js libraries" This reverts commit 1bba46d66117b4a96d279fd964a45fe673db658c. --- app/assets/javascripts/lib/utils/md5.js | 211 ------------------ .../javascripts/lib/utils/utf8_encode.js | 70 ------ 2 files changed, 281 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/md5.js delete mode 100644 app/assets/javascripts/lib/utils/utf8_encode.js diff --git a/app/assets/javascripts/lib/utils/md5.js b/app/assets/javascripts/lib/utils/md5.js deleted file mode 100644 index b63716eaad2..00000000000 --- a/app/assets/javascripts/lib/utils/md5.js +++ /dev/null @@ -1,211 +0,0 @@ -function md5 (str) { - // http://kevin.vanzonneveld.net - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + namespaced by: Michael White (http://getsprink.com) - // + tweaked by: Jack - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // - depends on: utf8_encode - // * example 1: md5('Kevin van Zonneveld'); - // * returns 1: '6e658d4bfcb59cc13f96c14450ac40b9' - var xl; - - var rotateLeft = function (lValue, iShiftBits) { - return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); - }; - - var addUnsigned = function (lX, lY) { - var lX4, lY4, lX8, lY8, lResult; - lX8 = (lX & 0x80000000); - lY8 = (lY & 0x80000000); - lX4 = (lX & 0x40000000); - lY4 = (lY & 0x40000000); - lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); - if (lX4 & lY4) { - return (lResult ^ 0x80000000 ^ lX8 ^ lY8); - } - if (lX4 | lY4) { - if (lResult & 0x40000000) { - return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); - } else { - return (lResult ^ 0x40000000 ^ lX8 ^ lY8); - } - } else { - return (lResult ^ lX8 ^ lY8); - } - }; - - var _F = function (x, y, z) { - return (x & y) | ((~x) & z); - }; - var _G = function (x, y, z) { - return (x & z) | (y & (~z)); - }; - var _H = function (x, y, z) { - return (x ^ y ^ z); - }; - var _I = function (x, y, z) { - return (y ^ (x | (~z))); - }; - - var _FF = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _GG = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _HH = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _II = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var convertToWordArray = function (str) { - var lWordCount; - var lMessageLength = str.length; - var lNumberOfWords_temp1 = lMessageLength + 8; - var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; - var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; - var lWordArray = new Array(lNumberOfWords - 1); - var lBytePosition = 0; - var lByteCount = 0; - while (lByteCount < lMessageLength) { - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition)); - lByteCount++; - } - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); - lWordArray[lNumberOfWords - 2] = lMessageLength << 3; - lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; - return lWordArray; - }; - - var wordToHex = function (lValue) { - var wordToHexValue = "", - wordToHexValue_temp = "", - lByte, lCount; - for (lCount = 0; lCount <= 3; lCount++) { - lByte = (lValue >>> (lCount * 8)) & 255; - wordToHexValue_temp = "0" + lByte.toString(16); - wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2); - } - return wordToHexValue; - }; - - var x = [], - k, AA, BB, CC, DD, a, b, c, d, S11 = 7, - S12 = 12, - S13 = 17, - S14 = 22, - S21 = 5, - S22 = 9, - S23 = 14, - S24 = 20, - S31 = 4, - S32 = 11, - S33 = 16, - S34 = 23, - S41 = 6, - S42 = 10, - S43 = 15, - S44 = 21; - - str = this.utf8_encode(str); - x = convertToWordArray(str); - a = 0x67452301; - b = 0xEFCDAB89; - c = 0x98BADCFE; - d = 0x10325476; - - xl = x.length; - for (k = 0; k < xl; k += 16) { - AA = a; - BB = b; - CC = c; - DD = d; - a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); - d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); - c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB); - b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); - a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); - d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); - c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613); - b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501); - a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8); - d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); - c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); - b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); - a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122); - d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193); - c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E); - b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821); - a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); - d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340); - c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); - b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); - a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); - d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453); - c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); - b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); - a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); - d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); - c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); - b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); - a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); - d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); - c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); - b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); - a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); - d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681); - c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); - b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); - a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); - d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); - c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); - b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); - a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); - d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); - c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); - b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05); - a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); - d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); - c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); - b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); - a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244); - d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97); - c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); - b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039); - a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3); - d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); - c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); - b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1); - a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); - d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); - c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314); - b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); - a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82); - d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); - c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); - b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391); - a = addUnsigned(a, AA); - b = addUnsigned(b, BB); - c = addUnsigned(c, CC); - d = addUnsigned(d, DD); - } - - var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d); - - return temp.toLowerCase(); -} diff --git a/app/assets/javascripts/lib/utils/utf8_encode.js b/app/assets/javascripts/lib/utils/utf8_encode.js deleted file mode 100644 index 39ffe44dae0..00000000000 --- a/app/assets/javascripts/lib/utils/utf8_encode.js +++ /dev/null @@ -1,70 +0,0 @@ -function utf8_encode (argString) { - // http://kevin.vanzonneveld.net - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + improved by: sowberry - // + tweaked by: Jack - // + bugfixed by: Onno Marsman - // + improved by: Yves Sucaet - // + bugfixed by: Onno Marsman - // + bugfixed by: Ulrich - // + bugfixed by: Rafal Kukawski - // + improved by: kirilloid - // + bugfixed by: kirilloid - // * example 1: utf8_encode('Kevin van Zonneveld'); - // * returns 1: 'Kevin van Zonneveld' - - if (argString === null || typeof argString === "undefined") { - return ""; - } - - var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - var utftext = '', - start, end, stringl = 0; - - start = end = 0; - stringl = string.length; - for (var n = 0; n < stringl; n++) { - var c1 = string.charCodeAt(n); - var enc = null; - - if (c1 < 128) { - end++; - } else if (c1 > 127 && c1 < 2048) { - enc = String.fromCharCode( - (c1 >> 6) | 192, - ( c1 & 63) | 128 - ); - } else if (c1 & 0xF800 != 0xD800) { - enc = String.fromCharCode( - (c1 >> 12) | 224, - ((c1 >> 6) & 63) | 128, - ( c1 & 63) | 128 - ); - } else { // surrogate pairs - if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); } - var c2 = string.charCodeAt(++n); - if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); } - c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000; - enc = String.fromCharCode( - (c1 >> 18) | 240, - ((c1 >> 12) & 63) | 128, - ((c1 >> 6) & 63) | 128, - ( c1 & 63) | 128 - ); - } - if (enc !== null) { - if (end > start) { - utftext += string.slice(start, end); - } - utftext += enc; - start = end = n + 1; - } - } - - if (end > start) { - utftext += string.slice(start, stringl); - } - - return utftext; -} From 4768afbdbf85abbb5e2281c8855e7d27c07a581e Mon Sep 17 00:00:00 2001 From: Keith Pope Date: Tue, 2 Aug 2016 06:56:23 +0100 Subject: [PATCH 167/198] Add simple identifier to public SSH keys --- CHANGELOG | 1 + app/models/key.rb | 5 +++-- spec/models/key_spec.rb | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9b66108c160..86bf05bfc08 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.11.0 (unreleased) - Fix renaming repository when name contains invalid chararacters under project settings - Optimize checking if a user has read access to a list of issues !5370 - Nokogiri's various parsing methods are now instrumented + - Add simple identifier to public SSH keys (muteor) - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Add build event color in HipChat messages (David Eisner) - Make fork counter always clickable. !5463 (winniehell) diff --git a/app/models/key.rb b/app/models/key.rb index b9bc38a0436..568a60b8af3 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -26,8 +26,9 @@ class Key < ActiveRecord::Base end def publishable_key - # Removes anything beyond the keytype and key itself - self.key.split[0..1].join(' ') + # Strip out the keys comment so we don't leak email addresses + # Replace with simple ident of user_name (hostname) + self.key.split[0..1].push("#{self.user_name} (#{Gitlab.config.gitlab.host})").join(' ') end # projects that has this key diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 49cf3d8633a..a4d46ca84de 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -16,12 +16,13 @@ describe Key, models: true do end describe "Methods" do + let(:user) { create(:user) } it { is_expected.to respond_to :projects } it { is_expected.to respond_to :publishable_key } describe "#publishable_keys" do - it 'strips all personal information' do - expect(build(:key).publishable_key).not_to match(/dummy@gitlab/) + it 'replaces SSH key comment with simple identifier of username + hostname' do + expect(build(:key, user: user).publishable_key).to match(/#{Regexp.escape(user.name)} \(localhost\)/) end end end From ab0aedef5b5b41135ce28490cedfaab13095f650 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 2 Aug 2016 11:56:47 +0200 Subject: [PATCH 168/198] Always compare with FETCH_HEAD in downtime_check This ensures this CI step works properly even when doing a shallow clone. --- lib/tasks/downtime_check.rake | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake index 30a2e9be5ce..afe5d42910c 100644 --- a/lib/tasks/downtime_check.rake +++ b/lib/tasks/downtime_check.rake @@ -1,26 +1,12 @@ desc 'Checks if migrations in a branch require downtime' task downtime_check: :environment do - # First we'll want to make sure we're comparing with the right upstream - # repository/branch. - current_branch = `git rev-parse --abbrev-ref HEAD`.strip - - # Either the developer ran this task directly on the master branch, or they're - # making changes directly on the master branch. - if current_branch == 'master' - if defined?(Gitlab::License) - repo = 'gitlab-ee' - else - repo = 'gitlab-ce' - end - - `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1` - - compare_with = 'FETCH_HEAD' - # The developer is working on a different branch, in this case we can just - # compare with the master branch. + if defined?(Gitlab::License) + repo = 'gitlab-ee' else - compare_with = 'master' + repo = 'gitlab-ce' end - Rake::Task['gitlab:db:downtime_check'].invoke(compare_with) + `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1` + + Rake::Task['gitlab:db:downtime_check'].invoke('FETCH_HEAD') end From f0b73f81198a9cba8961774f45e0b96f0cb73c78 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 31 May 2016 11:06:45 +0530 Subject: [PATCH 169/198] Add help document describing wiki linking behavior. --- app/views/projects/wikis/_form.html.haml | 2 + doc/markdown/markdown.md | 4 ++ doc/markdown/wiki.md | 77 ++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 doc/markdown/wiki.md diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 797a1a59e9f..893bea3c759 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -20,6 +20,8 @@ .help-block To link to a (new) page, simply type %code [Link Title](page-slug) + \. More examples are in the + = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') \. .form-group = f.label :commit_message, class: 'control-label' diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index c6c7ac81c0d..e4101fa5388 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -28,6 +28,10 @@ * [Line Breaks](#line-breaks) * [Tables](#tables) +**[Wiki-Specific Markdown](wiki.md)** + +* [Links](wiki.md#links-to-other-wiki-pages) + **[References](#references)** ## GitLab Flavored Markdown (GFM) diff --git a/doc/markdown/wiki.md b/doc/markdown/wiki.md new file mode 100644 index 00000000000..b58107abea3 --- /dev/null +++ b/doc/markdown/wiki.md @@ -0,0 +1,77 @@ +# Wiki-Specific Markdown + +## Table of Contents + +* [Links to Other Wiki Pages](#links-to-other-wiki-pages) + * [Direct Page Link](#direct-page-link) + * [Direct File Link](#direct-file-link) + * [Hierarchical Link](#hierarchical-link) + * [Root Link](#root-link) + +## Links to Other Wiki Pages + +You can link to other pages on your wiki in a few different ways. + +### Direct Page Link + +A link which just includes the slug for a page will point to that page, _at the base level of the wiki_. + +1. This snippet would link to a `documentation` page at the root of your wiki. + +```markdown +[Link to Documentation](documentation) +``` + +### Direct File Link + +Links with a file extension point to that file, _relative to the current page_. + +1. If this snippet was placed on a page at `/documentation/related`, it would link to `/documentation/file.md`. + + ```markdown + [Link to File](file.md) + ``` + +### Hierarchical Link + +A link can be constructed relative to the current wiki page using `./`, `../`, etc. + +1. If this snippet was placed on a page at `/documentation/main`, it would link to `/documentation/related`. + + ```markdown + [Link to Related Page](./related) + ``` + +1. If this snippet was placed on a page at `/documentation/related/content`, it would link to `/documentation/main`. + + ```markdown + [Link to Related Page](../main) + ``` + +1. If this snippet was placed on a page at `/documentation/main`, it would link to `/documentation/related.md`. + + ```markdown + [Link to Related Page](./related.md) + ``` + +1. If this snippet was placed on a page at `/documentation/related/content`, it would link to `/documentation/main.md`. + + ```markdown + [Link to Related Page](../main.md) + ``` + +### Root Link + +A link starting with a `/` is relative to the wiki root, for non-file links. + +1. This snippet links to `/documentation` + + ```markdown + [Link to Related Page](/documentation) + ``` + +1. This snippet links to `/miscellaneous.md` + + ```markdown + [Link to Related Page](/miscellaneous.md) + ``` From d55e83addecda6c6a3632826b9bb61437d18290f Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 31 May 2016 15:59:16 +0530 Subject: [PATCH 170/198] Use `succeed` to add periods to help text. --- app/views/projects/wikis/_form.html.haml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 893bea3c759..6360371804a 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -18,11 +18,14 @@ .error-alert .help-block - To link to a (new) page, simply type - %code [Link Title](page-slug) - \. More examples are in the - = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') - \. + = succeed '.' do + To link to a (new) page, simply type + %code [Link Title](page-slug) + + = succeed '.' do + More examples are in the + = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') + .form-group = f.label :commit_message, class: 'control-label' .col-sm-10= f.text_field :message, class: 'form-control', rows: 18 From dc396dc4f9622e96c0a1564260e106643eb48a4a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 2 Jun 2016 14:25:17 +0530 Subject: [PATCH 171/198] Remove the `non-file` qualifier for root links. --- doc/markdown/wiki.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/markdown/wiki.md b/doc/markdown/wiki.md index b58107abea3..fa9b3298edb 100644 --- a/doc/markdown/wiki.md +++ b/doc/markdown/wiki.md @@ -62,7 +62,7 @@ A link can be constructed relative to the current wiki page using `./`, `. ### Root Link -A link starting with a `/` is relative to the wiki root, for non-file links. +A link starting with a `/` is relative to the wiki root. 1. This snippet links to `/documentation` From f611b7b3573213956d8cd8f8580434dbcf3a41ea Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 2 Aug 2016 11:08:22 +0200 Subject: [PATCH 172/198] fix TODO comment [ci skip] --- lib/gitlab/import_export/relation_factory.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index e9c1b79fa45..5e56b3d1aa7 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -44,7 +44,7 @@ module Gitlab if @relation_name == :notes set_note_author - # TODO: note attatchments not supported yet + # attachment is deprecated and note uploads are handled by Markdown uploader @relation_hash['attachment'] = nil end From 97c61900e4bd764fa772f349a2996d8d940795b1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 2 Aug 2016 13:17:04 +0300 Subject: [PATCH 173/198] Refactor wiki Markdown documentation --- doc/markdown/wiki.md | 60 ++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/doc/markdown/wiki.md b/doc/markdown/wiki.md index fa9b3298edb..d9e6d071b9f 100644 --- a/doc/markdown/wiki.md +++ b/doc/markdown/wiki.md @@ -1,76 +1,86 @@ -# Wiki-Specific Markdown +# Wiki-specific Markdown -## Table of Contents +This page has information related to wiki-specific Markdown. For more +information on GitLab's Markdown, see the [main Markdown document](./markdown.md). -* [Links to Other Wiki Pages](#links-to-other-wiki-pages) - * [Direct Page Link](#direct-page-link) - * [Direct File Link](#direct-file-link) - * [Hierarchical Link](#hierarchical-link) - * [Root Link](#root-link) +## Table of contents -## Links to Other Wiki Pages +* [Links to other wiki pages](#links-to-other-wiki-pages) + * [Direct page link](#direct-page-link) + * [Direct file link](#direct-file-link) + * [Hierarchical link](#hierarchical-link) + * [Root link](#root-link) + +## Links to other wiki pages You can link to other pages on your wiki in a few different ways. -### Direct Page Link +### Direct page link -A link which just includes the slug for a page will point to that page, _at the base level of the wiki_. +A link which just includes the slug for a page will point to that page, +_at the base level of the wiki_. -1. This snippet would link to a `documentation` page at the root of your wiki. +This snippet would link to a `documentation` page at the root of your wiki: ```markdown [Link to Documentation](documentation) ``` -### Direct File Link +### Direct file link Links with a file extension point to that file, _relative to the current page_. -1. If this snippet was placed on a page at `/documentation/related`, it would link to `/documentation/file.md`. +If this snippet was placed on a page at `/documentation/related`, +it would link to `/documentation/file.md`: - ```markdown - [Link to File](file.md) - ``` +```markdown +[Link to File](file.md) +``` -### Hierarchical Link +### Hierarchical link -A link can be constructed relative to the current wiki page using `./`, `../`, etc. +A link can be constructed relative to the current wiki page using `./`, +`../`, etc. -1. If this snippet was placed on a page at `/documentation/main`, it would link to `/documentation/related`. +- If this snippet was placed on a page at `/documentation/main`, + it would link to `/documentation/related`: ```markdown [Link to Related Page](./related) ``` -1. If this snippet was placed on a page at `/documentation/related/content`, it would link to `/documentation/main`. +- If this snippet was placed on a page at `/documentation/related/content`, + it would link to `/documentation/main`: ```markdown [Link to Related Page](../main) ``` -1. If this snippet was placed on a page at `/documentation/main`, it would link to `/documentation/related.md`. +- If this snippet was placed on a page at `/documentation/main`, + it would link to `/documentation/related.md`: ```markdown [Link to Related Page](./related.md) ``` -1. If this snippet was placed on a page at `/documentation/related/content`, it would link to `/documentation/main.md`. +- If this snippet was placed on a page at `/documentation/related/content`, + it would link to `/documentation/main.md`: ```markdown [Link to Related Page](../main.md) ``` -### Root Link +### Root link A link starting with a `/` is relative to the wiki root. -1. This snippet links to `/documentation` +- This snippet links to `/documentation`: ```markdown [Link to Related Page](/documentation) ``` -1. This snippet links to `/miscellaneous.md` +- This snippet links to `/miscellaneous.md`: ```markdown [Link to Related Page](/miscellaneous.md) From 4611191785f2ca0eca2ba5be3b24224879bc5c50 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 2 Aug 2016 13:34:49 +0300 Subject: [PATCH 174/198] Merge wiki-specific Markdown in main Markdown file --- doc/markdown/markdown.md | 82 ++++++++++++++++++++++++++++++++++++- doc/markdown/wiki.md | 87 ---------------------------------------- 2 files changed, 80 insertions(+), 89 deletions(-) delete mode 100644 doc/markdown/wiki.md diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index e4101fa5388..d7f09ec070c 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -28,9 +28,12 @@ * [Line Breaks](#line-breaks) * [Tables](#tables) -**[Wiki-Specific Markdown](wiki.md)** +**[Wiki-Specific Markdown](#wiki-specific-markdown)** -* [Links](wiki.md#links-to-other-wiki-pages) +* [Wiki - Direct page link](#wiki-direct-page-link) +* [Wiki - Direct file link](#wiki-direct-file-link) +* [Wiki - Hierarchical link](#wiki-hierarchical-link) +* [Wiki - Root link](#wiki-root-link) **[References](#references)** @@ -696,6 +699,81 @@ By including colons in the header row, you can align the text within that column | Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | | Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | + +## Wiki-specific Markdown + +The following examples show how links inside wikis behave. + +### Wiki - Direct page link + +A link which just includes the slug for a page will point to that page, +_at the base level of the wiki_. + +This snippet would link to a `documentation` page at the root of your wiki: + +```markdown +[Link to Documentation](documentation) +``` + +### Wiki - Direct file link + +Links with a file extension point to that file, _relative to the current page_. + +If this snippet was placed on a page at `/documentation/related`, +it would link to `/documentation/file.md`: + +```markdown +[Link to File](file.md) +``` + +### Wiki - Hierarchical link + +A link can be constructed relative to the current wiki page using `./`, +`../`, etc. + +- If this snippet was placed on a page at `/documentation/main`, + it would link to `/documentation/related`: + + ```markdown + [Link to Related Page](./related) + ``` + +- If this snippet was placed on a page at `/documentation/related/content`, + it would link to `/documentation/main`: + + ```markdown + [Link to Related Page](../main) + ``` + +- If this snippet was placed on a page at `/documentation/main`, + it would link to `/documentation/related.md`: + + ```markdown + [Link to Related Page](./related.md) + ``` + +- If this snippet was placed on a page at `/documentation/related/content`, + it would link to `/documentation/main.md`: + + ```markdown + [Link to Related Page](../main.md) + ``` + +### Wiki - Root link + +A link starting with a `/` is relative to the wiki root. + +- This snippet links to `/documentation`: + + ```markdown + [Link to Related Page](/documentation) + ``` + +- This snippet links to `/miscellaneous.md`: + + ```markdown + [Link to Related Page](/miscellaneous.md) + ``` ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). diff --git a/doc/markdown/wiki.md b/doc/markdown/wiki.md deleted file mode 100644 index d9e6d071b9f..00000000000 --- a/doc/markdown/wiki.md +++ /dev/null @@ -1,87 +0,0 @@ -# Wiki-specific Markdown - -This page has information related to wiki-specific Markdown. For more -information on GitLab's Markdown, see the [main Markdown document](./markdown.md). - -## Table of contents - -* [Links to other wiki pages](#links-to-other-wiki-pages) - * [Direct page link](#direct-page-link) - * [Direct file link](#direct-file-link) - * [Hierarchical link](#hierarchical-link) - * [Root link](#root-link) - -## Links to other wiki pages - -You can link to other pages on your wiki in a few different ways. - -### Direct page link - -A link which just includes the slug for a page will point to that page, -_at the base level of the wiki_. - -This snippet would link to a `documentation` page at the root of your wiki: - -```markdown -[Link to Documentation](documentation) -``` - -### Direct file link - -Links with a file extension point to that file, _relative to the current page_. - -If this snippet was placed on a page at `/documentation/related`, -it would link to `/documentation/file.md`: - -```markdown -[Link to File](file.md) -``` - -### Hierarchical link - -A link can be constructed relative to the current wiki page using `./`, -`../`, etc. - -- If this snippet was placed on a page at `/documentation/main`, - it would link to `/documentation/related`: - - ```markdown - [Link to Related Page](./related) - ``` - -- If this snippet was placed on a page at `/documentation/related/content`, - it would link to `/documentation/main`: - - ```markdown - [Link to Related Page](../main) - ``` - -- If this snippet was placed on a page at `/documentation/main`, - it would link to `/documentation/related.md`: - - ```markdown - [Link to Related Page](./related.md) - ``` - -- If this snippet was placed on a page at `/documentation/related/content`, - it would link to `/documentation/main.md`: - - ```markdown - [Link to Related Page](../main.md) - ``` - -### Root link - -A link starting with a `/` is relative to the wiki root. - -- This snippet links to `/documentation`: - - ```markdown - [Link to Related Page](/documentation) - ``` - -- This snippet links to `/miscellaneous.md`: - - ```markdown - [Link to Related Page](/miscellaneous.md) - ``` From fc492c890596c996ad270dcbcd5d76c2b748cbeb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 2 Aug 2016 13:42:14 +0300 Subject: [PATCH 175/198] Change Markdown document location --- app/views/projects/wikis/_form.html.haml | 2 +- doc/README.md | 2 +- .../logo.png => user/project/img/markdown_logo.png} | Bin .../project/img/markdown_video.mp4} | Bin doc/{markdown => user/project}/markdown.md | 12 ++++++------ 5 files changed, 8 insertions(+), 8 deletions(-) rename doc/{markdown/img/logo.png => user/project/img/markdown_logo.png} (100%) rename doc/{markdown/img/video.mp4 => user/project/img/markdown_video.mp4} (100%) rename doc/{markdown => user/project}/markdown.md (99%) diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 6360371804a..643f7c589e6 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -24,7 +24,7 @@ = succeed '.' do More examples are in the - = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') + = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown") .form-group = f.label :commit_message, class: 'control-label' diff --git a/doc/README.md b/doc/README.md index b5b377822e6..751e685b19b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -9,7 +9,7 @@ - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). - [Importing and exporting projects between instances](user/project/settings/import_export.md). -- [Markdown](markdown/markdown.md) GitLab's advanced formatting system. +- [Markdown](user/project/markdown.md) GitLab's advanced formatting system. - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) diff --git a/doc/markdown/img/logo.png b/doc/user/project/img/markdown_logo.png similarity index 100% rename from doc/markdown/img/logo.png rename to doc/user/project/img/markdown_logo.png diff --git a/doc/markdown/img/video.mp4 b/doc/user/project/img/markdown_video.mp4 similarity index 100% rename from doc/markdown/img/video.mp4 rename to doc/user/project/img/markdown_video.mp4 diff --git a/doc/markdown/markdown.md b/doc/user/project/markdown.md similarity index 99% rename from doc/markdown/markdown.md rename to doc/user/project/markdown.md index d7f09ec070c..7fe96e67dbb 100644 --- a/doc/markdown/markdown.md +++ b/doc/user/project/markdown.md @@ -338,11 +338,11 @@ The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`. Here's a sample video: - ![Sample Video](img/video.mp4) + ![Sample Video](img/markdown_video.mp4) Here's a sample video: -![Sample Video](img/video.mp4) +![Sample Video](img/markdown_video.mp4) # Standard Markdown @@ -540,24 +540,24 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown Here's our logo (hover to see the title text): Inline-style: - ![alt text](img/logo.png) + ![alt text](img/markdown_logo.png) Reference-style: ![alt text1][logo] - [logo]: img/logo.png + [logo]: img/markdown_logo.png Here's our logo: Inline-style: -![alt text](img/logo.png) +![alt text](img/markdown_logo.png) Reference-style: ![alt text][logo] -[logo]: img/logo.png +[logo]: img/markdown_logo.png ## Blockquotes From 946d3b132e6f8e95b7d8b95e32fd8231b835137d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 2 Aug 2016 13:29:43 +0200 Subject: [PATCH 176/198] Instrument the Repository class Since this isn't an ActiveRecord::Base descendant it wasn't instrumented. --- CHANGELOG | 1 + config/initializers/metrics.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index c099c63ce86..42476248256 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 8.11.0 (unreleased) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - 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 - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index f3cddac5b36..b68a09ce730 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -144,6 +144,7 @@ if Gitlab::Metrics.enabled? end config.instrument_methods(Rinku) + config.instrument_instance_methods(Repository) end GC::Profiler.enable From b371d751287fd8a01126c7aa5f156f868d177ef2 Mon Sep 17 00:00:00 2001 From: Keith Pope Date: Tue, 2 Aug 2016 12:49:59 +0100 Subject: [PATCH 177/198] Tidy the key spec and fix failing user spec --- spec/models/key_spec.rb | 2 +- spec/models/user_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index a4d46ca84de..6d68e52a822 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -22,7 +22,7 @@ describe Key, models: true do describe "#publishable_keys" do it 'replaces SSH key comment with simple identifier of username + hostname' do - expect(build(:key, user: user).publishable_key).to match(/#{Regexp.escape(user.name)} \(localhost\)/) + expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)") end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2a5a7fb2fc6..9f432501c59 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -643,7 +643,7 @@ describe User, models: true do user = create :user key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id - expect(user.all_ssh_keys).to include(key.key) + expect(user.all_ssh_keys).to include(a_string_starting_with(key.key)) end end From 09257ce34c74a56bc08fd385d844a6d32b01e507 Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Tue, 2 Aug 2016 13:40:41 +0100 Subject: [PATCH 178/198] link the engineering workflow document from process and contributing --- CONTRIBUTING.md | 2 ++ PROCESS.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14ff05c9aa3..a885e706810 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,8 @@ abbreviation. If you have read this guide and want to know how the GitLab [core team] operates please see [the GitLab contributing process](PROCESS.md). +- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) + ## Contributor license agreement By submitting code as an individual you agree to the diff --git a/PROCESS.md b/PROCESS.md index fe3a963110d..8e1a3f7360f 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -8,6 +8,8 @@ treatment, etc.). And so that maintainers know what to expect from contributors (use the latest version, ensure that the issue is addressed, friendly treatment, etc.). +- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) + ## Common actions ### Issue team From 98a21cd4be0c2cf0f796fd8d602af0bef86cece3 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 2 Aug 2016 16:16:38 +0300 Subject: [PATCH 179/198] Remove outdated notes from the update guide --- doc/update/8.10-to-8.11.md | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index fc6262dd108..25343d484ba 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -62,23 +62,7 @@ sudo -u git -H git checkout v0.7.8 sudo -u git -H make ``` -### 6. Update MySQL permissions - -If you are using MySQL you need to grant the GitLab user the necessary -permissions on the database: - -```bash -# Login to MySQL -mysql -u root -p - -# Grant the GitLab user the REFERENCES permission on the database -GRANT REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost'; - -# Quit the database session -mysql> \q -``` - -### 7. Install libs, migrations, etc. +### 6. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -100,7 +84,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ``` -### 8. Update configuration files +### 7. Update configuration files #### New configuration options for `gitlab.yml` @@ -110,14 +94,6 @@ There are new configuration options available for [`gitlab.yml`](config/gitlab.y git diff origin/8-10-stable:config/gitlab.yml.example origin/8-11-stable:config/gitlab.yml.example ``` -#### Git configuration - -Disable `git gc --auto` because GitLab runs `git gc` for us already. - -```sh -sudo -u git -H git config --global gc.auto 0 -``` - #### Nginx configuration Ensure you're still up-to-date with the latest NGINX configuration changes: @@ -157,12 +133,12 @@ Ensure you're still up-to-date with the latest init script changes: sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab -### 9. Start application +### 8. Start application sudo service gitlab start sudo service nginx restart -### 10. Check application status +### 9. Check application status Check if GitLab and its environment are configured correctly: From 8716ff7f63fff0b056e110bef930c32a98e86c63 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Mon, 1 Aug 2016 16:55:51 +0200 Subject: [PATCH 180/198] Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible - Preloading noteable we share the same noteable instance when more than one discussion refers to the same noteable. - Any other call to that object that is cached in that object will be for any discussion. - In those cases where merge_request_diff has all the sha stored to build a diff_refs get that diff_refs using directly those sha instead accessing to the git repository to first get the commits and later the sha. --- CHANGELOG | 1 + .../projects/merge_requests_controller.rb | 2 ++ app/helpers/notes_helper.rb | 4 ++++ app/models/diff_note.rb | 12 ++++++++-- app/models/discussion.rb | 6 +++++ app/models/merge_request.rb | 15 +++++++++++- app/models/merge_request_diff.rb | 4 ++++ spec/models/diff_note_spec.rb | 4 ++-- spec/models/merge_request_spec.rb | 24 +++++++++++++++++++ 9 files changed, 67 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 42476248256..ece0a5c1e78 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -40,6 +40,7 @@ v 8.11.0 (unreleased) - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab. + - Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible - Add commit stats in commit api. !5517 (dixpac) - Make error pages responsive (Takuya Noguchi) - Change requests_profiles resource constraint to catch virtually any file diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 03166294ddd..116e7904a4e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -378,6 +378,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController fresh. discussions + preload_noteable_for_regular_notes(@discussions.flat_map(&:notes)) + # This is not executed lazily @notes = Banzai::NoteRenderer.render( @discussions.flat_map(&:notes), diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 0c47abe0fba..26bde2230a9 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -92,6 +92,10 @@ module NotesHelper project.team.max_member_access_for_user_ids(user_ids) end + def preload_noteable_for_regular_notes(notes) + ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable) + end + def note_max_access_for_user(note) note.project.team.human_max_access(note.author_id) end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 9671955db36..c816deb4e0c 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -67,7 +67,7 @@ class DiffNote < Note return false unless supported? return true if for_commit? - diff_refs ||= self.noteable.diff_refs + diff_refs ||= noteable_diff_refs self.position.diff_refs == diff_refs end @@ -78,6 +78,14 @@ class DiffNote < Note !self.for_merge_request? || self.noteable.support_new_diff_notes? end + def noteable_diff_refs + if noteable.respond_to?(:diff_sha_refs) + noteable.diff_sha_refs + else + noteable.diff_refs + end + end + def set_original_position self.original_position = self.position.dup end @@ -96,7 +104,7 @@ class DiffNote < Note self.project, nil, old_diff_refs: self.position.diff_refs, - new_diff_refs: self.noteable.diff_refs, + new_diff_refs: noteable_diff_refs, paths: self.position.paths ).execute(self) end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 74facfd1c9c..e2218a5f02b 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -49,6 +49,12 @@ class Discussion self.noteable == target && !diff_discussion? end + def active? + return @active if defined?(@active) + + @active = first_note.active? + end + def expanded? !diff_discussion? || active? end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f1b9c1d6feb..a99c4ba52a4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -255,6 +255,19 @@ class MergeRequest < ActiveRecord::Base ) end + # Return diff_refs instance trying to not touch the git repository + def diff_sha_refs + if merge_request_diff && merge_request_diff.diff_refs_by_sha? + return Gitlab::Diff::DiffRefs.new( + base_sha: merge_request_diff.base_commit_sha, + start_sha: merge_request_diff.start_commit_sha, + head_sha: merge_request_diff.head_commit_sha + ) + else + diff_refs + end + end + def validate_branches if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" @@ -659,7 +672,7 @@ class MergeRequest < ActiveRecord::Base end def support_new_diff_notes? - diff_refs && diff_refs.complete? + diff_sha_refs && diff_sha_refs.complete? end def update_diff_notes_positions(old_diff_refs:, new_diff_refs:) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 3f520c8f3ff..119266f2d2c 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -82,6 +82,10 @@ class MergeRequestDiff < ActiveRecord::Base project.commit(self.head_commit_sha) end + def diff_refs_by_sha? + base_commit_sha? && head_commit_sha? && start_commit_sha? + end + def compare @compare ||= begin diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index af8e890ca95..1fa96eb1f15 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -119,7 +119,7 @@ describe DiffNote, models: true do context "when the merge request's diff refs don't match that of the diff note" do before do - allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) + allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs) end it "returns false" do @@ -168,7 +168,7 @@ describe DiffNote, models: true do context "when the note is outdated" do before do - allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) + allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs) end it "uses the DiffPositionUpdateService" do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a0e3c26e542..21d22c776e9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -686,4 +686,28 @@ describe MergeRequest, models: true do subject.reload_diff end end + + describe "#diff_sha_refs" do + context "with diffs" do + subject { create(:merge_request, :with_diffs) } + + it "does not touch the repository" do + subject # Instantiate the object + + expect_any_instance_of(Repository).not_to receive(:commit) + + subject.diff_sha_refs + end + + it "returns expected diff_refs" do + expected_diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: subject.merge_request_diff.base_commit_sha, + start_sha: subject.merge_request_diff.start_commit_sha, + head_sha: subject.merge_request_diff.head_commit_sha + ) + + expect(subject.diff_sha_refs).to eq(expected_diff_refs) + end + end + end end From d707c91f706a26ff30b9d4862bb2c50b45a35fff Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 1 Aug 2016 22:57:15 -0700 Subject: [PATCH 181/198] Add guide for debugging issues with the Docker container registry [ci skip] --- doc/container_registry/README.md | 4 + .../img/mitmproxy-docker.png | Bin 0 -> 407004 bytes doc/container_registry/troubleshooting.md | 139 ++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 doc/container_registry/img/mitmproxy-docker.png create mode 100644 doc/container_registry/troubleshooting.md diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md index 55077197ff9..3db351811a8 100644 --- a/doc/container_registry/README.md +++ b/doc/container_registry/README.md @@ -90,6 +90,10 @@ your `.gitlab-ci.yml`, you have to follow the [Using a private Docker Registry][private-docker] documentation. This workflow will be simplified in the future. +## Troubleshooting + +See [the GitLab Docker registry troubleshooting guide](troubleshooting.md). + [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ [private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry diff --git a/doc/container_registry/img/mitmproxy-docker.png b/doc/container_registry/img/mitmproxy-docker.png new file mode 100644 index 0000000000000000000000000000000000000000..4e3e37b413d8c8c8d15ee85c6d7057dfa5397e11 GIT binary patch literal 407004 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 z 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

*t1YcOsV&oIed^_#$(ldvp;m+R-cZNB!0&)zN# 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%0Ar9S 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ⓈZLce>e@qrM+;gY%k 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

I1+hSm$U>R*GLF;gci`+0Uqs%8cp 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 literal 0 HcmV?d00001 diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md new file mode 100644 index 00000000000..e21fc9a2c61 --- /dev/null +++ b/doc/container_registry/troubleshooting.md @@ -0,0 +1,139 @@ +# Troubleshooting the GitLab Container Registry + +## Basic Troubleshooting + +1. Check to make sure that the system clock on your Docker client and GitLab server have + been synchronized (e.g. via NTP). + +2. If you are using an S3-backed registry, double check that the IAM + permissions and the S3 credentials (including region) are correct. See [the + sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/) + for more details. + +3. Check the registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs + for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues + there. + +# Advanced Troubleshooting + +NOTE: The following section is only recommended for experts. + +Sometimes it's not obvious what is wrong, and you may need to dive deeper into +the communication between the Docker client and the registry to find out +what's wrong. We will use a concrete example in the past to illustrate how to +diagnose a problem with the S3 setup. + +## Example: Unexpected 403 error during push + +A user attempted to enable an S3-backed registry. The `docker login` step went +fine. However, when pushing an image, the output showed: + +``` +The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test] +dc5e59c14160: Pushing [==================================================>] 14.85 kB +03c20c1a019a: Pushing [==================================================>] 2.048 kB +a08f14ef632e: Pushing [==================================================>] 2.048 kB +228950524c88: Pushing 2.048 kB +6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB +5f70bf18a086: Pushing 1.024 kB +737f40e80b7f: Waiting +82b57dbc5385: Waiting +19429b698a22: Waiting +9436069b92a3: Waiting +error parsing HTTP 403 response body: unexpected end of JSON input: "" +``` + +This error is ambiguous, as it's not clear whether the 403 is coming from the GitLab Rails +application, the Docker registry, or something else. In this case, we know that since +the login succeeded, we probably need to look at the communication between the client +and the registry. + +The REST API between the Docker client and registry is [described +here](https://docs.docker.com/registry/spec/api/). Normally, one would just +use Wireshark or tcpdump to capture the traffic and see where things went +wrong. However, since all communication between Docker clients and servers +are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even +if you know the private key. What can we do instead? + +## mitmproxy + +Enter [mitmproxy](https://mitmproxy.org/). This tool allows you to place a +proxy between your client and server to inspect all traffic. One wrinkle is +that your system needs to trust the mitmproxy SSL certificates for this +to work. + +The following installation instructions assume you are running Ubuntu: + +1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html) + +2. Run `mitmproxy --port 9000` to generate its certificates. Enter CTRL-C to quit. + +3. Install the certificate from ~/.mitmproxy to your system: + + ```sh + sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt + sudo update-ca-certificates + ``` + +If successful, the output should indicate that a certificate was added: + +```sh +Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done. +Running hooks in /etc/ca-certificates/update.d....done. +``` + +## Verifying mitmproxy certifiactes + +To verify that the certificates are properly install, run: + +```sh +mitmproxy --port 9000 +``` + +This will run mitmproxy on port 9000. In another window, run: + +```sh +curl --proxy http://localhost:9000 https://httpbin.org/status/200 +``` + +If everything is setup correctly, then you will see information on the mitmproxy window and +no errors from the curl commands. + +## Running the Docker daemon with a proxy + +For Docker to connect through a proxy, you must start the Docker daemon with the +proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`) +and then run Docker by hand. As root, run: + +```sh +export HTTP_PROXY="http://localhost:9000" +export HTTPS_PROXY="https://localhost:9000" +docker daemon --debug +``` + +This will launch the Docker daemon and proxy all connections through mitmproxy. + +## Running the Docker client + +Now that we have mitmproxy and Docker running, we can now attempt to login and push a container +image. You may need to run as root to do this. For example: + +```sh +docker login s3-testing.myregistry.com:4567 +docker push s3-testing.myregistry.com:4567/root/docker-test +``` + +In the example above, we see the following trace on the mitmproxy window: + +![mitmproxy output from Docker](img/mitmproxy-docker.png) + +The above image shows: + +* The initial PUT requests went through fine with a 201 status code. +* The 201 redirected the client to the S3 bucket. +* The HEAD request to the AWS bucket reported a 403 Unauthorized. + +What does this mean? This strongly suggests that the S3 user does not have the right +[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html). +The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/). +Once the right permissions were set, the error went away. From 0ee33149fc7375201e14e79a6027e577d59c66a0 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Tue, 2 Aug 2016 09:47:05 -0500 Subject: [PATCH 182/198] Add and update templates for 8.11 --- vendor/gitignore/Elm.gitignore | 2 +- vendor/gitignore/Go.gitignore | 3 ++ vendor/gitignore/Leiningen.gitignore | 3 +- vendor/gitignore/Objective-C.gitignore | 2 +- vendor/gitignore/Scala.gitignore | 4 ++ vendor/gitignore/SugarCRM.gitignore | 2 + vendor/gitignore/TeX.gitignore | 11 +++++ vendor/gitignore/Terraform.gitignore | 3 ++ vendor/gitignore/Unity.gitignore | 3 +- vendor/gitlab-ci-yml/C++.gitlab-ci.yml | 26 ++++++++++++ vendor/gitlab-ci-yml/Grails.gitlab-ci.yml | 40 +++++++++++++++++++ vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml | 11 +++++ .../gitlab-ci-yml/Pages/JBake.gitlab-ci.yml | 32 +++++++++++++++ vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml | 2 + 14 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 vendor/gitlab-ci-yml/C++.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/Grails.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml diff --git a/vendor/gitignore/Elm.gitignore b/vendor/gitignore/Elm.gitignore index a594364e2c0..8b631e7de00 100644 --- a/vendor/gitignore/Elm.gitignore +++ b/vendor/gitignore/Elm.gitignore @@ -1,4 +1,4 @@ # elm-package generated files -elm-stuff/ +elm-stuff # elm-repl generated files repl-temp-* diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore index daf913b1b34..cd0d5d1e2f4 100644 --- a/vendor/gitignore/Go.gitignore +++ b/vendor/gitignore/Go.gitignore @@ -22,3 +22,6 @@ _testmain.go *.exe *.test *.prof + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/vendor/gitignore/Leiningen.gitignore b/vendor/gitignore/Leiningen.gitignore index 47fed6c20d9..a9fe6fba80d 100644 --- a/vendor/gitignore/Leiningen.gitignore +++ b/vendor/gitignore/Leiningen.gitignore @@ -1,6 +1,7 @@ pom.xml pom.xml.asc -*jar +*.jar +*.class /lib/ /classes/ /target/ diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore index 86f21d8e0ff..20592083931 100644 --- a/vendor/gitignore/Objective-C.gitignore +++ b/vendor/gitignore/Objective-C.gitignore @@ -52,7 +52,7 @@ Carthage/Build fastlane/report.xml fastlane/screenshots -#Code Injection +# Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore index c58d83b3189..a02d882cb88 100644 --- a/vendor/gitignore/Scala.gitignore +++ b/vendor/gitignore/Scala.gitignore @@ -15,3 +15,7 @@ project/plugins/project/ # Scala-IDE specific .scala_dependencies .worksheet + +# ENSIME specific +.ensime_cache/ +.ensime diff --git a/vendor/gitignore/SugarCRM.gitignore b/vendor/gitignore/SugarCRM.gitignore index 842c3ec518b..e9270205fd5 100644 --- a/vendor/gitignore/SugarCRM.gitignore +++ b/vendor/gitignore/SugarCRM.gitignore @@ -7,6 +7,7 @@ # For development the cache directory can be safely ignored and # therefore it is ignored. /cache/ +!/cache/index.html # Ignore some files and directories from the custom directory. /custom/history/ /custom/modulebuilder/ @@ -22,4 +23,5 @@ *.log # Ignore the new upload directories. /upload/ +!/upload/index.html /upload_backup/ diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index 3cb097c9d5e..34f999df3e7 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -19,6 +19,9 @@ # *.eps # *.pdf +## Generated if empty string is given at "Please type another file name for output:" +.pdf + ## Bibliography auxiliary files (bibtex/biblatex/biber): *.bbl *.bcf @@ -31,6 +34,7 @@ ## Build tool auxiliary files: *.fdb_latexmk *.synctex +*.synctex(busy) *.synctex.gz *.synctex.gz(busy) *.pdfsync @@ -84,6 +88,10 @@ acs-*.bib # gnuplottex *-gnuplottex-* +# gregoriotex +*.gaux +*.gtex + # hyperref *.brf @@ -128,6 +136,9 @@ _minted* *.sagetex.py *.sagetex.scmd +# scrwfile +*.wrt + # sympy *.sout *.sympy diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore index 7868d16d216..41859c81f1c 100644 --- a/vendor/gitignore/Terraform.gitignore +++ b/vendor/gitignore/Terraform.gitignore @@ -1,3 +1,6 @@ # Compiled files *.tfstate *.tfstate.backup + +# Module directory +.terraform/ diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore index 5aafcbb7f1d..1c10388911b 100644 --- a/vendor/gitignore/Unity.gitignore +++ b/vendor/gitignore/Unity.gitignore @@ -5,8 +5,9 @@ /[Bb]uilds/ /Assets/AssetStoreTools* -# Autogenerated VS/MD solution and project files +# Autogenerated VS/MD/Consulo solution and project files ExportedObj/ +.consulo/ *.csproj *.unityproj *.sln diff --git a/vendor/gitlab-ci-yml/C++.gitlab-ci.yml b/vendor/gitlab-ci-yml/C++.gitlab-ci.yml new file mode 100644 index 00000000000..c83c49d8c95 --- /dev/null +++ b/vendor/gitlab-ci-yml/C++.gitlab-ci.yml @@ -0,0 +1,26 @@ +# use the official gcc image, based on debian +# can use verions as well, like gcc:5.2 +# see https://hub.docker.com/_/gcc/ +image: gcc + +build: + stage: build + # instead of calling g++ directly you can also use some build toolkit like make + # install the necessary build tools when needed + # before_script: + # - apt update && apt -y install make autoconf + script: + - g++ helloworld.cpp -o mybinary + artifacts: + paths: + - mybinary + # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time + # cache: + # paths: + # - "*.o" + +# run tests using the binary built before +test: + stage: test + script: + - ./runmytests.sh diff --git a/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml b/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml new file mode 100644 index 00000000000..7fc698d50cf --- /dev/null +++ b/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml @@ -0,0 +1,40 @@ +# This template uses the java:8 docker image because there isn't any +# official Grails image at this moment +# +# Grails Framework https://grails.org/ is a powerful Groovy-based web application framework for the JVM +# +# This yml works with Grails 3.x only +# Feel free to change GRAILS_VERSION version with your project version (3.0.1, 3.1.1,...) +# Feel free to change GRADLE_VERSION version with your gradle project version (2.13, 2.14,...) +# If you use Angular profile, this yml it's prepared to work with it + +image: java:8 + +variables: + GRAILS_VERSION: "3.1.9" + GRADLE_VERSION: "2.13" + +# We use SDKMan as tool for managing versions +before_script: + - apt-get update -qq && apt-get install -y -qq unzip + - curl -sSL https://get.sdkman.io | bash + - echo sdkman_auto_answer=true > /root/.sdkman/etc/config + - source /root/.sdkman/bin/sdkman-init.sh + - sdk install gradle $GRADLE_VERSION < /dev/null + - sdk use gradle $GRADLE_VERSION +# As it's not a good idea to version gradle.properties feel free to add your +# environments variable here + - echo grailsVersion=$GRAILS_VERSION > gradle.properties + - echo gradleWrapperVersion=2.14 >> gradle.properties +# refresh dependencies from your project + - ./gradlew --refresh-dependencies +# Be aware that if you are using Angular profile, +# Bower cannot be run as root if you don't allow it before. +# Feel free to remove next line if you are not using Bower + - echo {\"allow_root\":true} > /root/.bowerrc + +# This build job does the full grails pipeline +# (compile, test, integrationTest, war, assemble). +build: + script: + - ./gradlew build \ No newline at end of file diff --git a/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml b/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml new file mode 100644 index 00000000000..a4aed36889e --- /dev/null +++ b/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml @@ -0,0 +1,11 @@ +# use docker image with latex preinstalled +# since there is no official latex image, use https://github.com/blang/latex-docker +# possible alternative: https://github.com/natlownes/docker-latex +image: blang/latex + +build: + script: + - latexmk -pdf + artifacts: + paths: + - "*.pdf" diff --git a/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml new file mode 100644 index 00000000000..bc36a4e6966 --- /dev/null +++ b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml @@ -0,0 +1,32 @@ +# This template uses the java:8 docker image because there isn't any +# official JBake image at this moment +# +# JBake https://jbake.org/ is a Java based, open source, static site/blog generator for developers & designers +# +# This yml works with jBake 2.4.0 +# Feel free to change JBAKE_VERSION version +# +# HowTo at: https://jorge.aguilera.gitlab.io/howtojbake/ + +image: java:8 + +variables: + JBAKE_VERSION: 2.4.0 + + +# We use SDKMan as tool for managing versions +before_script: + - apt-get update -qq && apt-get install -y -qq unzip + - curl -sSL https://get.sdkman.io | bash + - echo sdkman_auto_answer=true > /root/.sdkman/etc/config + - source /root/.sdkman/bin/sdkman-init.sh + - sdk install jbake $JBAKE_VERSION < /dev/null + - sdk use jbake $JBAKE_VERSION + +# This build job produced the output directory of your site +pages: + script: + - jbake . public + artifacts: + paths: + - public \ No newline at end of file diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml index 2a761bbd127..16a685ee03d 100644 --- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml @@ -19,6 +19,8 @@ cache: # services such as redis or postgres before_script: - ruby -v # Print out ruby version for debugging + # Uncomment next line if your rails app needs a JS runtime: + # - apt-get update -q && apt-get install nodejs -yqq - gem install bundler --no-ri --no-rdoc # Bundler is not installed with the image - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby From e4c517a635b6a45a9afc65b37682cc4b4951e922 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 25 Jul 2016 23:49:06 -0500 Subject: [PATCH 183/198] Expand commit message width in repo view --- CHANGELOG | 1 + app/assets/stylesheets/pages/tree.scss | 4 ++++ app/models/commit.rb | 14 +++++++------ .../tree/_tree_commit_column.html.haml | 2 +- spec/models/commit_spec.rb | 21 +++++++++++++++++++ 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 963fec597d5..b44627a9a71 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,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 + - Expand commit message width in repo view (ClemMakesApps) - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 390977297fb..9da40fe2b09 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -58,6 +58,10 @@ .tree_commit { max-width: 320px; + + .str-truncated { + max-width: 100%; + } } .tree_time_ago { diff --git a/app/models/commit.rb b/app/models/commit.rb index 486ad6714d9..c52b4a051c2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -123,15 +123,17 @@ class Commit # In case this first line is longer than 100 characters, it is cut off # after 80 characters and ellipses (`&hellp;`) are appended. def title - title = safe_message + full_title.length > 100 ? full_title[0..79] << "…" : full_title + end - return no_commit_message if title.blank? + # Returns the full commits title + def full_title + return @full_title if @full_title - title_end = title.index("\n") - if (!title_end && title.length > 100) || (title_end && title_end > 100) - title[0..79] << "…" + if safe_message.blank? + @full_title = no_commit_message else - title.split("\n", 2).first + @full_title = safe_message.split("\n", 2).first end end diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index a3a4bd4f752..84da16b6bb1 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" + = link_to_gfm commit.full_title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index c3392ee7440..d3e6a6648cc 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -86,6 +86,27 @@ eos end end + describe '#full_title' do + it "returns no_commit_message when safe_message is blank" do + allow(commit).to receive(:safe_message).and_return('') + expect(commit.full_title).to eq("--no commit message") + end + + it "returns entire message if there is no newline" do + message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' + + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.full_title).to eq(message) + end + + it "returns first line of message if there is a newLine" do + message = commit.safe_message.split(" ").first + + allow(commit).to receive(:safe_message).and_return(message + "\n" + message) + expect(commit.full_title).to eq(message) + end + end + describe "delegation" do subject { commit } From 46385e4e5a88a4ac614f680094b9226778cee64a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 2 Aug 2016 15:20:36 -0700 Subject: [PATCH 184/198] Add a note about setting up an insecure registry [ci skip] --- doc/container_registry/troubleshooting.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md index e21fc9a2c61..c24c80518dd 100644 --- a/doc/container_registry/troubleshooting.md +++ b/doc/container_registry/troubleshooting.md @@ -55,12 +55,17 @@ wrong. However, since all communication between Docker clients and servers are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even if you know the private key. What can we do instead? +One way would be to disable HTTPS by setting up an [insecure +registry](https://docs.docker.com/registry/insecure/). This could introduce a +security hole and is only recommended for local testing. If you have a +production system and can't or don't want to do this, there is another way: +use mitmproxy, which stands for Man-in-the-Middle Proxy. + ## mitmproxy -Enter [mitmproxy](https://mitmproxy.org/). This tool allows you to place a -proxy between your client and server to inspect all traffic. One wrinkle is -that your system needs to trust the mitmproxy SSL certificates for this -to work. +[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your +client and server to inspect all traffic. One wrinkle is that your system +needs to trust the mitmproxy SSL certificates for this to work. The following installation instructions assume you are running Ubuntu: From 2c9cce0feb8bd4e10f3406493eff30e783782d15 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 2 Aug 2016 15:24:15 -0700 Subject: [PATCH 185/198] Grammar improvements [ci skip] --- doc/container_registry/troubleshooting.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md index c24c80518dd..8008bf29935 100644 --- a/doc/container_registry/troubleshooting.md +++ b/doc/container_registry/troubleshooting.md @@ -43,10 +43,10 @@ a08f14ef632e: Pushing [==================================================>] 2.04 error parsing HTTP 403 response body: unexpected end of JSON input: "" ``` -This error is ambiguous, as it's not clear whether the 403 is coming from the GitLab Rails -application, the Docker registry, or something else. In this case, we know that since -the login succeeded, we probably need to look at the communication between the client -and the registry. +This error is ambiguous, as it's not clear whether the 403 is coming from the +GitLab Rails application, the Docker registry, or something else. In this +case, since we know that since the login succeeded, we probably need to look +at the communication between the client and the registry. The REST API between the Docker client and registry is [described here](https://docs.docker.com/registry/spec/api/). Normally, one would just From 51bcdfb7850a642c1062d5ab73417a6c6d2edb51 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 2 Aug 2016 18:01:14 -0500 Subject: [PATCH 186/198] Fix filter input alignment --- CHANGELOG | 1 + app/assets/stylesheets/framework/nav.scss | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 31e5d982cdf..21e02d61af9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.11.0 (unreleased) - Optimize checking if a user has read access to a list of issues !5370 - Nokogiri's various parsing methods are now instrumented - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 + - Fix filter input alignment (ClemMakesApps) - Include old revision in merge request update hooks (Ben Boeckel) - Add build event color in HipChat messages (David Eisner) - Make fork counter always clickable. !5463 (winniehell) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 364952d3b4a..7852fc9a424 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -182,7 +182,6 @@ > form { display: inline-block; - margin-top: -1px; } .icon-label { @@ -193,7 +192,6 @@ height: 35px; display: inline-block; position: relative; - top: 2px; margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ From cd7c2cb6ddd4d9c9f9bdae00c887c0022c121c17 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 20 Jul 2016 18:25:36 +0200 Subject: [PATCH 187/198] Cache highlighted diff lines for merge requests Introducing the concept of SafeDiffs which relates diffs with UI highlighting. --- CHANGELOG | 1 + app/controllers/concerns/diff_for_path.rb | 6 +- app/controllers/projects/commit_controller.rb | 4 +- .../projects/compare_controller.rb | 6 +- .../projects/merge_requests_controller.rb | 10 +++- app/helpers/commits_helper.rb | 4 +- app/helpers/diff_helper.rb | 11 ++-- app/models/merge_request.rb | 2 + app/models/safe_diffs.rb | 5 ++ app/models/safe_diffs/base.rb | 55 +++++++++++++++++++ app/models/safe_diffs/commit.rb | 10 ++++ app/models/safe_diffs/compare.rb | 10 ++++ app/models/safe_diffs/merge_request.rb | 52 ++++++++++++++++++ .../merge_request_diff_cache_service.rb | 8 +++ .../notify/repository_push_email.html.haml | 2 +- app/views/projects/commit/show.html.haml | 2 +- app/views/projects/compare/show.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 2 - app/views/projects/diffs/_file.html.haml | 4 +- app/views/projects/diffs/_text_file.html.haml | 2 +- .../merge_requests/_new_submit.html.haml | 2 +- .../merge_requests/show/_diffs.html.haml | 5 +- lib/gitlab/diff/file.rb | 5 +- lib/gitlab/diff/highlight.rb | 7 ++- lib/gitlab/diff/line.rb | 14 +++++ lib/gitlab/email/message/repository_push.rb | 2 +- .../projects/commit_controller_spec.rb | 13 +++-- .../projects/compare_controller_spec.rb | 14 ++--- .../merge_requests_controller_spec.rb | 18 +++--- 29 files changed, 221 insertions(+), 57 deletions(-) create mode 100644 app/models/safe_diffs.rb create mode 100644 app/models/safe_diffs/base.rb create mode 100644 app/models/safe_diffs/commit.rb create mode 100644 app/models/safe_diffs/compare.rb create mode 100644 app/models/safe_diffs/merge_request.rb create mode 100644 app/services/merge_requests/merge_request_diff_cache_service.rb diff --git a/CHANGELOG b/CHANGELOG index db2617dcbd7..581b8b09af9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 8.11.0 (unreleased) - The Repository class is now instrumented - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - 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' - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb index 026d8b2e1e0..aeec3009f15 100644 --- a/app/controllers/concerns/diff_for_path.rb +++ b/app/controllers/concerns/diff_for_path.rb @@ -1,8 +1,8 @@ module DiffForPath extend ActiveSupport::Concern - def render_diff_for_path(diffs, diff_refs, project) - diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff| + def render_diff_for_path(diffs) + diff_file = diffs.diff_files.find do |diff| diff.old_path == params[:old_path] && diff.new_path == params[:new_path] end @@ -14,7 +14,7 @@ module DiffForPath locals = { diff_file: diff_file, diff_commit: diff_commit, - diff_refs: diff_refs, + diff_refs: diffs.diff_refs, blob: blob, project: project } diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 7ae034f9398..6060b6e55bc 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -28,7 +28,7 @@ class Projects::CommitController < Projects::ApplicationController end def diff_for_path - render_diff_for_path(@diffs, @commit.diff_refs, @project) + render_diff_for_path(SafeDiffs::Commit.new(@commit, diff_options: diff_options)) end def builds @@ -110,7 +110,7 @@ class Projects::CommitController < Projects::ApplicationController opts = diff_options opts[:ignore_whitespace_change] = true if params[:format] == 'diff' - @diffs = commit.diffs(opts) + @diffs = SafeDiffs::Commit.new(commit, diff_options: opts) @notes_count = commit.notes.count end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 8c004724f02..2eda950a1bd 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(@diffs, @diff_refs, @project) + render_diff_for_path(SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options)) end def create @@ -46,12 +46,12 @@ class Projects::CompareController < Projects::ApplicationController @commit = @project.commit(@head_ref) @base_commit = @project.merge_base_commit(@start_ref, @head_ref) - @diffs = @compare.diffs(diff_options) - @diff_refs = Gitlab::Diff::DiffRefs.new( + diff_refs = Gitlab::Diff::DiffRefs.new( base_sha: @base_commit.try(:sha), start_sha: @start_commit.try(:sha), head_sha: @commit.try(:sha) ) + @diffs = SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options, diff_refs: diff_refs) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 116e7904a4e..78a6a3c5715 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -103,9 +103,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end define_commit_vars - diffs = @merge_request.diffs(diff_options) - render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project) + render_diff_for_path(SafeDiffs::MergeRequest.new(merge_request, diff_options: diff_options)) end def commits @@ -153,7 +152,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.compare_commits.reverse @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit - @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare + if @merge_request.compare + @diffs = SafeDiffs::Compare.new(@merge_request.compare, + project: @merge_request.project, + diff_refs: @merge_request.diff_refs, + diff_options: diff_options) + end @diff_notes_disabled = true @pipeline = @merge_request.pipeline diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index f497626e21a..7a02d0b10d9 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -206,10 +206,10 @@ module CommitsHelper end end - def view_file_btn(commit_sha, diff, project) + def view_file_btn(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, - tree_join(commit_sha, diff.new_path)), + tree_join(commit_sha, diff_new_path)), class: 'btn view-file js-view-file btn-file-option' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f35e2f6ddcd..6497282af57 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -23,18 +23,17 @@ module DiffHelper end def diff_options - options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? } + options = SafeDiffs.default_options.merge( + ignore_whitespace_change: hide_whitespace?, + no_collapse: expand_all_diffs? + ) if action_name == 'diff_for_path' options[:no_collapse] = true options[:paths] = params.values_at(:old_path, :new_path) end - Commit.max_diff_options.merge(options) - end - - def safe_diff_files(diffs, diff_refs: nil, repository: nil) - diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } + options end def unfold_bottom_class(bottom) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a99c4ba52a4..774851cc90f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -313,6 +313,8 @@ class MergeRequest < ActiveRecord::Base merge_request_diff.reload_content + MergeRequests::MergeRequestDiffCacheService.new.execute(self) + new_diff_refs = self.diff_refs update_diff_notes_positions( diff --git a/app/models/safe_diffs.rb b/app/models/safe_diffs.rb new file mode 100644 index 00000000000..8ca9ec4cc39 --- /dev/null +++ b/app/models/safe_diffs.rb @@ -0,0 +1,5 @@ +module SafeDiffs + def self.default_options + ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) + end +end diff --git a/app/models/safe_diffs/base.rb b/app/models/safe_diffs/base.rb new file mode 100644 index 00000000000..dfc4708e293 --- /dev/null +++ b/app/models/safe_diffs/base.rb @@ -0,0 +1,55 @@ +module SafeDiffs + class Base + attr_reader :project, :diff_options, :diff_view, :diff_refs + + delegate :count, :real_size, to: :diff_files + + def initialize(diffs, project:, diff_options:, diff_refs: nil) + @diffs = diffs + @project = project + @diff_options = diff_options + @diff_refs = diff_refs + end + + def diff_files + @diff_files ||= begin + diffs = @diffs.decorate! do |diff| + Gitlab::Diff::File.new(diff, diff_refs: @diff_refs, repository: @project.repository) + end + + highlight!(diffs) + diffs + end + end + + private + + def highlight!(diff_files) + if cacheable? + cache_highlight!(diff_files) + else + diff_files.each { |diff_file| highlight_diff_file!(diff_file) } + end + end + + def cacheable? + false + end + + def cache_highlight! + raise NotImplementedError + end + + def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) + diff_file.diff_lines = cache_diff_lines.map do |line| + Gitlab::Diff::Line.init_from_hash(line) + end + end + + def highlight_diff_file!(diff_file) + diff_file.diff_lines = Gitlab::Diff::Highlight.new(diff_file, repository: diff_file.repository).highlight + diff_file.highlighted_diff_lines = diff_file.diff_lines # To be used on parallel diff + diff_file + end + end +end diff --git a/app/models/safe_diffs/commit.rb b/app/models/safe_diffs/commit.rb new file mode 100644 index 00000000000..338878f32e0 --- /dev/null +++ b/app/models/safe_diffs/commit.rb @@ -0,0 +1,10 @@ +module SafeDiffs + class Commit < Base + def initialize(commit, diff_options:) + super(commit.diffs(diff_options), + project: commit.project, + diff_options: diff_options, + diff_refs: commit.diff_refs) + end + end +end diff --git a/app/models/safe_diffs/compare.rb b/app/models/safe_diffs/compare.rb new file mode 100644 index 00000000000..6b64b81137d --- /dev/null +++ b/app/models/safe_diffs/compare.rb @@ -0,0 +1,10 @@ +module SafeDiffs + class Compare < Base + def initialize(compare, project:, diff_options:, diff_refs: nil) + super(compare.diffs(diff_options), + project: project, + diff_options: diff_options, + diff_refs: diff_refs) + end + end +end diff --git a/app/models/safe_diffs/merge_request.rb b/app/models/safe_diffs/merge_request.rb new file mode 100644 index 00000000000..111b9a54f91 --- /dev/null +++ b/app/models/safe_diffs/merge_request.rb @@ -0,0 +1,52 @@ +module SafeDiffs + class MergeRequest < Base + def initialize(merge_request, diff_options:) + @merge_request = merge_request + + super(merge_request.diffs(diff_options), + project: merge_request.project, + diff_options: diff_options, + diff_refs: merge_request.diff_refs) + end + + private + + # + # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted) + # for the highlighted ones, so we just skip their execution. + # If the highlighted diff files lines are not cached we calculate and cache them. + # + # The content of the cache is and Hash where the key correspond to the file_path and the values are Arrays of + # hashes than represent serialized diff lines. + # + def cache_highlight!(diff_files) + highlighted_cache = Rails.cache.read(cache_key) || {} + highlighted_cache_was_empty = highlighted_cache.empty? + + diff_files.each do |diff_file| + file_path = diff_file.file_path + + if highlighted_cache[file_path] + highlight_diff_file_from_cache!(diff_file, highlighted_cache[file_path]) + else + highlight_diff_file!(diff_file) + highlighted_cache[file_path] = diff_file.diff_lines.map(&:to_hash) + end + end + + if highlighted_cache_was_empty + Rails.cache.write(cache_key, highlighted_cache) + end + + diff_files + end + + def cacheable? + @merge_request.merge_request_diff.present? + end + + def cache_key + [@merge_request.merge_request_diff, 'highlighted-safe-diff-files', diff_options] + end + end +end diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb new file mode 100644 index 00000000000..0a1905f7137 --- /dev/null +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -0,0 +1,8 @@ +module MergeRequests + class MergeRequestDiffCacheService + def execute(merge_request) + # Executing the iteration we cache all the highlighted diff information + SafeDiffs::MergeRequest.new(merge_request, diff_options: SafeDiffs.default_options).diff_files.to_a + end + end +end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index c161ecc3463..2d1a98caeaa 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -75,7 +75,7 @@ - blob = diff_file.blob - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) %table.code.white - - diff_file.highlighted_diff_lines.each do |line| + - diff_file.diff_lines.each do |line| = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - else No preview for this file type diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index d0da2606587..11b2020f99b 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -7,7 +7,7 @@ = render "ci_menu" - else %div.block-connector -= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs += render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 28a50e7031a..eb8a1bd5289 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs + = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs - else .light-well .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 4bf3ccace20..45895a9a3de 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,8 +2,6 @@ - if diff_view == 'parallel' - fluid_layout true -- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository) - .content-block.oneline-block.files-changed .inline-parallel-buttons - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 1854c64cbd7..f914e13a1ec 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -15,6 +15,6 @@ from_merge_request_id: @merge_request.id, skip_visible_check: true) - = view_file_btn(diff_commit.id, diff_file, project) + = view_file_btn(diff_commit.id, diff_file.new_path, project) - = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project + = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 5970b9abf2b..a483927671e 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -5,7 +5,7 @@ %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } - last_line = 0 - - diff_file.highlighted_diff_lines.each do |line| + - diff_file.diff_lines.each do |line| - last_line = line.new_pos = render "projects/diffs/line", line: line, diff_file: diff_file diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index a5e67b95727..cb2b623691c 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -42,7 +42,7 @@ %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %p To preserve performance the line changes are not shown. - else - = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false + = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false - if @pipeline #builds.builds.tab-pane = render "projects/merge_requests/show/builds" diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 1b0bae86ad4..ed2765356db 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,6 +1,7 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options), - project: @merge_request.project, diff_refs: @merge_request.diff_refs + - diffs = SafeDiffs::MergeRequest.new(@merge_request, diff_options: diff_options) + = render "projects/diffs/diffs", diff_files: diffs.diff_files, + diff_refs: diffs.diff_refs, project: diffs.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index b09ca1fb8b0..77b3798d78f 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -63,15 +63,18 @@ module Gitlab diff_refs.try(:head_sha) end + attr_writer :diff_lines, :highlighted_diff_lines + # Array of Gitlab::Diff::Line objects def diff_lines - @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a + @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a end def highlighted_diff_lines @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight end + # Array[] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted def parallel_diff_lines @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 649a265a02c..9ea976e18fa 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -40,8 +40,6 @@ module Gitlab def highlight_line(diff_line) return unless diff_file && diff_file.diff_refs - line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' - rich_line = if diff_line.unchanged? || diff_line.added? new_lines[diff_line.new_pos - 1] @@ -51,7 +49,10 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. - "#{line_prefix}#{rich_line}".html_safe if rich_line + if rich_line + line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' + "#{line_prefix}#{rich_line}".html_safe + end end def inline_diffs diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index c6189d660c2..cf097e0d0de 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -9,6 +9,20 @@ module Gitlab @old_pos, @new_pos = old_pos, new_pos end + def self.init_from_hash(hash) + new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos]) + end + + def serialize_keys + @serialize_keys ||= %i(text type index old_pos new_pos) + end + + def to_hash + hash = {} + serialize_keys.each { |key| hash[key] = send(key) } + hash + end + def old_line old_pos unless added? || meta? end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 97701b0cd42..48946ba355b 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -41,7 +41,7 @@ module Gitlab def diffs return unless compare - @diffs ||= safe_diff_files(compare.diffs(max_files: 30), diff_refs: diff_refs, repository: project.repository) + @diffs ||= SafeDiffs::Compare.new(compare, diff_options: { max_files: 30 }, project: project, diff_refs: diff_refs).diff_files end def diffs_count diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index df902da86f8..30121facd7d 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -83,7 +83,7 @@ describe Projects::CommitController do let(:format) { :diff } it "should really only be a git diff" do - go(id: commit.id, format: format) + go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format) expect(response.body).to start_with("diff --git") end @@ -92,8 +92,9 @@ describe Projects::CommitController do go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1) expect(response.body).to start_with("diff --git") - # without whitespace option, there are more than 2 diff_splits - diff_splits = assigns(:diffs).first.diff.split("\n") + + # without whitespace option, there are more than 2 diff_splits for other formats + diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n") expect(diff_splits.length).to be <= 2 end end @@ -266,9 +267,9 @@ describe Projects::CommitController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| - expect(diffs.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs, diff_refs, project) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| + expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(safe_diffs) end diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 4058d5e2453..6272a5f111d 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -19,7 +19,7 @@ describe Projects::CompareController do to: ref_to) expect(response).to be_success - expect(assigns(:diffs).first).not_to be_nil + expect(assigns(:diffs).diff_files.first).not_to be_nil expect(assigns(:commits).length).to be >= 1 end @@ -32,10 +32,10 @@ describe Projects::CompareController do w: 1) expect(response).to be_success - expect(assigns(:diffs).first).not_to be_nil + expect(assigns(:diffs).diff_files.first).not_to be_nil expect(assigns(:commits).length).to be >= 1 # without whitespace option, there are more than 2 diff_splits - diff_splits = assigns(:diffs).first.diff.split("\n") + diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n") expect(diff_splits.length).to be <= 2 end @@ -48,7 +48,7 @@ describe Projects::CompareController do to: ref_to) expect(response).to be_success - expect(assigns(:diffs).to_a).to eq([]) + expect(assigns(:diffs).diff_files.to_a).to eq([]) expect(assigns(:commits)).to eq([]) end @@ -87,9 +87,9 @@ describe Projects::CompareController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| - expect(diffs.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs, diff_refs, project) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| + expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(safe_diffs) end diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 210085e3b1a..9da43c9cee7 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -392,9 +392,9 @@ describe Projects::MergeRequestsController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| - expect(diffs.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs, diff_refs, project) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| + expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(safe_diffs) end diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) @@ -455,9 +455,9 @@ describe Projects::MergeRequestsController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| - expect(diffs.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs, diff_refs, project) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| + expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(safe_diffs) end diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) @@ -477,9 +477,9 @@ describe Projects::MergeRequestsController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| - expect(diffs.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs, diff_refs, project) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| + expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(safe_diffs) end diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) From 8f359ea9170b984ad43d126e17628c31ac3a1f14 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Tue, 26 Jul 2016 09:21:42 +0200 Subject: [PATCH 188/198] Move to Gitlab::Diff::FileCollection Instead calling diff_collection.count use diff_collection.size which is cache on the diff_collection --- app/controllers/projects/commit_controller.rb | 4 +- .../projects/compare_controller.rb | 4 +- .../projects/merge_requests_controller.rb | 16 ++-- app/helpers/diff_helper.rb | 2 +- app/models/commit.rb | 4 + app/models/compare.rb | 23 +++++ app/models/merge_request.rb | 4 + app/models/safe_diffs.rb | 5 -- app/models/safe_diffs/base.rb | 55 ------------ app/models/safe_diffs/commit.rb | 10 --- app/models/safe_diffs/compare.rb | 10 --- app/models/safe_diffs/merge_request.rb | 52 ----------- .../merge_request_diff_cache_service.rb | 2 +- .../notify/repository_push_email.html.haml | 2 +- app/views/projects/commit/_ci_menu.html.haml | 2 +- app/views/projects/diffs/_text_file.html.haml | 2 +- .../merge_requests/show/_diffs.html.haml | 5 +- app/workers/irker_worker.rb | 2 +- lib/gitlab/diff/file.rb | 2 +- lib/gitlab/diff/file_collection.rb | 9 ++ lib/gitlab/diff/file_collection/base.rb | 30 +++++++ lib/gitlab/diff/file_collection/commit.rb | 14 +++ lib/gitlab/diff/file_collection/compare.rb | 14 +++ .../diff/file_collection/merge_request.rb | 88 +++++++++++++++++++ lib/gitlab/email/message/repository_push.rb | 10 ++- .../projects/commit_controller_spec.rb | 6 +- .../projects/compare_controller_spec.rb | 11 +-- .../merge_requests_controller_spec.rb | 18 ++-- .../email/message/repository_push_spec.rb | 4 +- spec/models/merge_request_spec.rb | 6 ++ .../merge_request_diff_cache_service_spec.rb | 18 ++++ 31 files changed, 259 insertions(+), 175 deletions(-) create mode 100644 app/models/compare.rb delete mode 100644 app/models/safe_diffs.rb delete mode 100644 app/models/safe_diffs/base.rb delete mode 100644 app/models/safe_diffs/commit.rb delete mode 100644 app/models/safe_diffs/compare.rb delete mode 100644 app/models/safe_diffs/merge_request.rb create mode 100644 lib/gitlab/diff/file_collection.rb create mode 100644 lib/gitlab/diff/file_collection/base.rb create mode 100644 lib/gitlab/diff/file_collection/commit.rb create mode 100644 lib/gitlab/diff/file_collection/compare.rb create mode 100644 lib/gitlab/diff/file_collection/merge_request.rb create mode 100644 spec/services/merge_requests/merge_request_diff_cache_service_spec.rb diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 6060b6e55bc..771a86530cd 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -28,7 +28,7 @@ class Projects::CommitController < Projects::ApplicationController end def diff_for_path - render_diff_for_path(SafeDiffs::Commit.new(@commit, diff_options: diff_options)) + render_diff_for_path(@commit.diff_file_collection(diff_options)) end def builds @@ -110,7 +110,7 @@ class Projects::CommitController < Projects::ApplicationController opts = diff_options opts[:ignore_whitespace_change] = true if params[:format] == 'diff' - @diffs = SafeDiffs::Commit.new(commit, diff_options: opts) + @diffs = commit.diff_file_collection(opts) @notes_count = commit.notes.count end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 2eda950a1bd..252ddfa429a 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options)) + render_diff_for_path(Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options)) end def create @@ -51,7 +51,7 @@ class Projects::CompareController < Projects::ApplicationController start_sha: @start_commit.try(:sha), head_sha: @commit.try(:sha) ) - @diffs = SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options, diff_refs: diff_refs) + @diffs = Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 78a6a3c5715..39e7d0f6182 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -85,7 +85,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html { define_discussion_vars } - format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } } + format.json do + @diffs = @merge_request.diff_file_collection(diff_options) if @merge_request_diff.collected? + + render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } + end end end @@ -104,7 +108,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController define_commit_vars - render_diff_for_path(SafeDiffs::MergeRequest.new(merge_request, diff_options: diff_options)) + render_diff_for_path(@merge_request.diff_file_collection(diff_options)) end def commits @@ -153,10 +157,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit if @merge_request.compare - @diffs = SafeDiffs::Compare.new(@merge_request.compare, - project: @merge_request.project, - diff_refs: @merge_request.diff_refs, - diff_options: diff_options) + @diffs = Compare.decorate(@merge_request.compare, @project).diff_file_collection( + diff_options: diff_options, + diff_refs: @merge_request.diff_refs + ) end @diff_notes_disabled = true diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 6497282af57..2abe24b78bf 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -23,7 +23,7 @@ module DiffHelper end def diff_options - options = SafeDiffs.default_options.merge( + options = Gitlab::Diff::FileCollection.default_options.merge( ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? ) diff --git a/app/models/commit.rb b/app/models/commit.rb index c52b4a051c2..d22ecb222e5 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -317,6 +317,10 @@ class Commit nil end + def diff_file_collection(diff_options) + Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) + end + private def find_author_by_any_email diff --git a/app/models/compare.rb b/app/models/compare.rb new file mode 100644 index 00000000000..6672d1bf059 --- /dev/null +++ b/app/models/compare.rb @@ -0,0 +1,23 @@ +class Compare + delegate :commits, :same, :head, :base, to: :@compare + + def self.decorate(compare, project) + if compare.is_a?(Compare) + compare + else + self.new(compare, project) + end + end + + def initialize(compare, project) + @compare = compare + @project = project + end + + def diff_file_collection(diff_options:, diff_refs: nil) + Gitlab::Diff::FileCollection::Compare.new(@compare, + project: @project, + diff_options: diff_options, + diff_refs: diff_refs) + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 774851cc90f..abc8bacbe59 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -168,6 +168,10 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) end + def diff_file_collection(diff_options) + Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) + end + def diff_size merge_request_diff.size end diff --git a/app/models/safe_diffs.rb b/app/models/safe_diffs.rb deleted file mode 100644 index 8ca9ec4cc39..00000000000 --- a/app/models/safe_diffs.rb +++ /dev/null @@ -1,5 +0,0 @@ -module SafeDiffs - def self.default_options - ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) - end -end diff --git a/app/models/safe_diffs/base.rb b/app/models/safe_diffs/base.rb deleted file mode 100644 index dfc4708e293..00000000000 --- a/app/models/safe_diffs/base.rb +++ /dev/null @@ -1,55 +0,0 @@ -module SafeDiffs - class Base - attr_reader :project, :diff_options, :diff_view, :diff_refs - - delegate :count, :real_size, to: :diff_files - - def initialize(diffs, project:, diff_options:, diff_refs: nil) - @diffs = diffs - @project = project - @diff_options = diff_options - @diff_refs = diff_refs - end - - def diff_files - @diff_files ||= begin - diffs = @diffs.decorate! do |diff| - Gitlab::Diff::File.new(diff, diff_refs: @diff_refs, repository: @project.repository) - end - - highlight!(diffs) - diffs - end - end - - private - - def highlight!(diff_files) - if cacheable? - cache_highlight!(diff_files) - else - diff_files.each { |diff_file| highlight_diff_file!(diff_file) } - end - end - - def cacheable? - false - end - - def cache_highlight! - raise NotImplementedError - end - - def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) - diff_file.diff_lines = cache_diff_lines.map do |line| - Gitlab::Diff::Line.init_from_hash(line) - end - end - - def highlight_diff_file!(diff_file) - diff_file.diff_lines = Gitlab::Diff::Highlight.new(diff_file, repository: diff_file.repository).highlight - diff_file.highlighted_diff_lines = diff_file.diff_lines # To be used on parallel diff - diff_file - end - end -end diff --git a/app/models/safe_diffs/commit.rb b/app/models/safe_diffs/commit.rb deleted file mode 100644 index 338878f32e0..00000000000 --- a/app/models/safe_diffs/commit.rb +++ /dev/null @@ -1,10 +0,0 @@ -module SafeDiffs - class Commit < Base - def initialize(commit, diff_options:) - super(commit.diffs(diff_options), - project: commit.project, - diff_options: diff_options, - diff_refs: commit.diff_refs) - end - end -end diff --git a/app/models/safe_diffs/compare.rb b/app/models/safe_diffs/compare.rb deleted file mode 100644 index 6b64b81137d..00000000000 --- a/app/models/safe_diffs/compare.rb +++ /dev/null @@ -1,10 +0,0 @@ -module SafeDiffs - class Compare < Base - def initialize(compare, project:, diff_options:, diff_refs: nil) - super(compare.diffs(diff_options), - project: project, - diff_options: diff_options, - diff_refs: diff_refs) - end - end -end diff --git a/app/models/safe_diffs/merge_request.rb b/app/models/safe_diffs/merge_request.rb deleted file mode 100644 index 111b9a54f91..00000000000 --- a/app/models/safe_diffs/merge_request.rb +++ /dev/null @@ -1,52 +0,0 @@ -module SafeDiffs - class MergeRequest < Base - def initialize(merge_request, diff_options:) - @merge_request = merge_request - - super(merge_request.diffs(diff_options), - project: merge_request.project, - diff_options: diff_options, - diff_refs: merge_request.diff_refs) - end - - private - - # - # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted) - # for the highlighted ones, so we just skip their execution. - # If the highlighted diff files lines are not cached we calculate and cache them. - # - # The content of the cache is and Hash where the key correspond to the file_path and the values are Arrays of - # hashes than represent serialized diff lines. - # - def cache_highlight!(diff_files) - highlighted_cache = Rails.cache.read(cache_key) || {} - highlighted_cache_was_empty = highlighted_cache.empty? - - diff_files.each do |diff_file| - file_path = diff_file.file_path - - if highlighted_cache[file_path] - highlight_diff_file_from_cache!(diff_file, highlighted_cache[file_path]) - else - highlight_diff_file!(diff_file) - highlighted_cache[file_path] = diff_file.diff_lines.map(&:to_hash) - end - end - - if highlighted_cache_was_empty - Rails.cache.write(cache_key, highlighted_cache) - end - - diff_files - end - - def cacheable? - @merge_request.merge_request_diff.present? - end - - def cache_key - [@merge_request.merge_request_diff, 'highlighted-safe-diff-files', diff_options] - end - end -end diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb index 0a1905f7137..982540ba7f5 100644 --- a/app/services/merge_requests/merge_request_diff_cache_service.rb +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -2,7 +2,7 @@ module MergeRequests class MergeRequestDiffCacheService def execute(merge_request) # Executing the iteration we cache all the highlighted diff information - SafeDiffs::MergeRequest.new(merge_request, diff_options: SafeDiffs.default_options).diff_files.to_a + merge_request.diff_file_collection(Gitlab::Diff::FileCollection.default_options).diff_files.to_a end end end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 2d1a98caeaa..c161ecc3463 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -75,7 +75,7 @@ - blob = diff_file.blob - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) %table.code.white - - diff_file.diff_lines.each do |line| + - diff_file.highlighted_diff_lines.each do |line| = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - else No preview for this file type diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index ea33aa472a6..935433306ea 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -2,7 +2,7 @@ = nav_link(path: 'commit#show') do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes - %span.badge= @diffs.count + %span.badge= @diffs.size = nav_link(path: 'commit#builds') do = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Builds diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index a483927671e..5970b9abf2b 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -5,7 +5,7 @@ %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } - last_line = 0 - - diff_file.diff_lines.each do |line| + - diff_file.highlighted_diff_lines.each do |line| - last_line = line.new_pos = render "projects/diffs/line", line: line, diff_file: diff_file diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index ed2765356db..5b842dd9280 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,7 +1,6 @@ - if @merge_request_diff.collected? - - diffs = SafeDiffs::MergeRequest.new(@merge_request, diff_options: diff_options) - = render "projects/diffs/diffs", diff_files: diffs.diff_files, - diff_refs: diffs.diff_refs, project: diffs.project + = render "projects/diffs/diffs", diff_files: @diffs.diff_files, + diff_refs: @diffs.diff_refs, project: @diffs.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 605ec4f04e5..a3c34e02baa 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -142,7 +142,7 @@ class IrkerWorker def files_count(commit) files = "#{commit.diffs.real_size} file" - files += 's' if commit.diffs.count > 1 + files += 's' if commit.diffs.size > 1 files end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 77b3798d78f..e47df508ca2 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -63,7 +63,7 @@ module Gitlab diff_refs.try(:head_sha) end - attr_writer :diff_lines, :highlighted_diff_lines + attr_writer :highlighted_diff_lines # Array of Gitlab::Diff::Line objects def diff_lines diff --git a/lib/gitlab/diff/file_collection.rb b/lib/gitlab/diff/file_collection.rb new file mode 100644 index 00000000000..ce6717c7205 --- /dev/null +++ b/lib/gitlab/diff/file_collection.rb @@ -0,0 +1,9 @@ +module Gitlab + module Diff + module FileCollection + def self.default_options + ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) + end + end + end +end diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb new file mode 100644 index 00000000000..20562773c14 --- /dev/null +++ b/lib/gitlab/diff/file_collection/base.rb @@ -0,0 +1,30 @@ +module Gitlab + module Diff + module FileCollection + + class Base + attr_reader :project, :diff_options, :diff_view, :diff_refs + + delegate :count, :size, :real_size, to: :diff_files + + def initialize(diffs, project:, diff_options:, diff_refs: nil) + @diffs = diffs + @project = project + @diff_options = diff_options + @diff_refs = diff_refs + end + + def diff_files + @diffs.decorate! { |diff| decorate_diff!(diff) } + end + + private + + def decorate_diff!(diff) + return diff if diff.is_a?(Gitlab::Diff::File) + Gitlab::Diff::File.new(diff, diff_refs: @diff_refs, repository: @project.repository) + end + end + end + end +end diff --git a/lib/gitlab/diff/file_collection/commit.rb b/lib/gitlab/diff/file_collection/commit.rb new file mode 100644 index 00000000000..2a46109ad99 --- /dev/null +++ b/lib/gitlab/diff/file_collection/commit.rb @@ -0,0 +1,14 @@ +module Gitlab + module Diff + module FileCollection + class Commit < Base + def initialize(commit, diff_options:) + super(commit.diffs(diff_options), + project: commit.project, + diff_options: diff_options, + diff_refs: commit.diff_refs) + end + end + end + end +end diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb new file mode 100644 index 00000000000..1bcda145f15 --- /dev/null +++ b/lib/gitlab/diff/file_collection/compare.rb @@ -0,0 +1,14 @@ +module Gitlab + module Diff + module FileCollection + class Compare < Base + def initialize(compare, project:, diff_options:, diff_refs: nil) + super(compare.diffs(diff_options), + project: project, + diff_options: diff_options, + diff_refs: diff_refs) + end + end + end + end +end diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request.rb new file mode 100644 index 00000000000..7c40622d594 --- /dev/null +++ b/lib/gitlab/diff/file_collection/merge_request.rb @@ -0,0 +1,88 @@ +module Gitlab + module Diff + module FileCollection + class MergeRequest < Base + def initialize(merge_request, diff_options:) + @merge_request = merge_request + + super(merge_request.diffs(diff_options), + project: merge_request.project, + diff_options: diff_options, + diff_refs: merge_request.diff_refs) + end + + def diff_files + super.tap { |_| store_highlight_cache } + end + + private + + # Extracted method to highlight in the same iteration to the diff_collection. Iteration in the DiffCollections + # seems particularly slow on big diffs (event when already populated). + def decorate_diff!(diff) + highlight! super + end + + def highlight!(diff_file) + if cacheable? + cache_highlight!(diff_file) + else + highlight_diff_file!(diff_file) + end + end + + def highlight_diff_file!(diff_file) + diff_file.highlighted_diff_lines = Gitlab::Diff::Highlight.new(diff_file, repository: diff_file.repository).highlight + diff_file + end + + def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) + diff_file.highlighted_diff_lines = cache_diff_lines.map do |line| + Gitlab::Diff::Line.init_from_hash(line) + end + end + + # + # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted) + # for the highlighted ones, so we just skip their execution. + # If the highlighted diff files lines are not cached we calculate and cache them. + # + # The content of the cache is a Hash where the key correspond to the file_path and the values are Arrays of + # hashes that represent serialized diff lines. + # + def cache_highlight!(diff_file) + file_path = diff_file.file_path + + if highlight_cache[file_path] + highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path]) + else + highlight_diff_file!(diff_file) + highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash) + end + + diff_file + end + + def highlight_cache + return @highlight_cache if defined?(@highlight_cache) + + @highlight_cache = Rails.cache.read(cache_key) || {} + @highlight_cache_was_empty = highlight_cache.empty? + @highlight_cache + end + + def store_highlight_cache + Rails.cache.write(cache_key, highlight_cache) if @highlight_cache_was_empty + end + + def cacheable? + @merge_request.merge_request_diff.present? + end + + def cache_key + [@merge_request.merge_request_diff, 'highlighted-diff-files', diff_options] + end + end + end + end +end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 48946ba355b..71213813e17 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -40,16 +40,18 @@ module Gitlab def diffs return unless compare - - @diffs ||= SafeDiffs::Compare.new(compare, diff_options: { max_files: 30 }, project: project, diff_refs: diff_refs).diff_files + + @diffs ||= compare.diff_file_collection(diff_options: { max_files: 30 }, diff_refs: diff_refs).diff_files end def diffs_count - diffs.count if diffs + diffs.size if diffs end def compare - @opts[:compare] + if @opts[:compare] + Compare.decorate(@opts[:compare], project) + end end def diff_refs diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 30121facd7d..940019b708b 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -267,9 +267,9 @@ describe Projects::CommitController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| - expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(safe_diffs) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) end diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 6272a5f111d..ed4cc36de58 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -32,10 +32,11 @@ describe Projects::CompareController do w: 1) expect(response).to be_success - expect(assigns(:diffs).diff_files.first).not_to be_nil + diff_file = assigns(:diffs).diff_files.first + expect(diff_file).not_to be_nil expect(assigns(:commits).length).to be >= 1 # without whitespace option, there are more than 2 diff_splits - diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n") + diff_splits = diff_file.diff.diff.split("\n") expect(diff_splits.length).to be <= 2 end @@ -87,9 +88,9 @@ describe Projects::CompareController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| - expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(safe_diffs) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) end diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 9da43c9cee7..1f6bc84dfe8 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -392,9 +392,9 @@ describe Projects::MergeRequestsController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| - expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(safe_diffs) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) end diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) @@ -455,9 +455,9 @@ describe Projects::MergeRequestsController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| - expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(safe_diffs) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) end diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) @@ -477,9 +477,9 @@ describe Projects::MergeRequestsController do end it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, safe_diffs| - expect(safe_diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(safe_diffs) + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) end diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index c19f33e2224..c1d07329983 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -62,12 +62,12 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#diffs_count' do subject { message.diffs_count } - it { is_expected.to eq compare.diffs.count } + it { is_expected.to eq compare.diffs.size } end describe '#compare' do subject { message.compare } - it { is_expected.to be_an_instance_of Gitlab::Git::Compare } + it { is_expected.to be_an_instance_of Compare } end describe '#compare_timeout' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 21d22c776e9..fa1f7edae8e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -660,6 +660,12 @@ describe MergeRequest, models: true do subject.reload_diff end + it "executs diff cache service" do + expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject) + + subject.reload_diff + end + it "updates diff note positions" do old_diff_refs = subject.diff_refs diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb new file mode 100644 index 00000000000..c6cceed31ad --- /dev/null +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe MergeRequests::MergeRequestDiffCacheService do + + let(:subject) { MergeRequests::MergeRequestDiffCacheService.new } + + describe '#execute' do + it 'retrieve the diff files to cache the highlighted result' do + merge_request = create(:merge_request) + cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection.default_options] + + expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) + expect(Rails.cache).to receive(:write).with(cache_key, anything) + + subject.execute(merge_request) + end + end +end From 1d0c7b74920a94e488e6a2c090abb3e525438053 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 27 Jul 2016 13:09:52 +0200 Subject: [PATCH 189/198] Introduce Compare model in the codebase. This object will manage Gitlab::Git::Compare instances --- .../projects/compare_controller.rb | 18 +++---- .../projects/merge_requests_controller.rb | 7 +-- app/models/commit.rb | 2 +- app/models/compare.rb | 47 ++++++++++++++++++- app/models/merge_request.rb | 8 +++- app/services/compare_service.rb | 7 ++- app/services/merge_requests/build_service.rb | 2 +- .../merge_request_diff_cache_service.rb | 2 +- app/views/projects/commit/show.html.haml | 2 +- app/views/projects/compare/show.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 7 +-- .../merge_requests/_new_submit.html.haml | 2 +- .../merge_requests/show/_diffs.html.haml | 2 +- app/workers/emails_on_push_worker.rb | 20 +++----- lib/gitlab/diff/file_collection/base.rb | 2 +- lib/gitlab/diff/file_collection/commit.rb | 3 ++ lib/gitlab/diff/file_collection/compare.rb | 3 ++ .../diff/file_collection/merge_request.rb | 10 ++-- lib/gitlab/email/message/repository_push.rb | 18 +++---- .../email/message/repository_push_spec.rb | 11 +++-- spec/mailers/notify_spec.rb | 10 ++-- .../merge_requests/build_service_spec.rb | 8 ++-- 22 files changed, 118 insertions(+), 75 deletions(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 252ddfa429a..7fca5e77f32 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options)) + render_diff_for_path(@compare.diff_file_collection(diff_options: diff_options)) end def create @@ -40,18 +40,12 @@ class Projects::CompareController < Projects::ApplicationController @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref) if @compare - @commits = Commit.decorate(@compare.commits, @project) + @commits = @compare.commits + @start_commit = @compare.start_commit + @commit = @compare.commit + @base_commit = @compare.base_commit - @start_commit = @project.commit(@start_ref) - @commit = @project.commit(@head_ref) - @base_commit = @project.merge_base_commit(@start_ref, @head_ref) - - diff_refs = Gitlab::Diff::DiffRefs.new( - base_sha: @base_commit.try(:sha), - start_sha: @start_commit.try(:sha), - head_sha: @commit.try(:sha) - ) - @diffs = Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) + @diffs = @compare.diff_file_collection(diff_options: diff_options) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 39e7d0f6182..20afc6afcb2 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -86,7 +86,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html { define_discussion_vars } format.json do - @diffs = @merge_request.diff_file_collection(diff_options) if @merge_request_diff.collected? + @diffs = @merge_request.diff_file_collection(diff_options) render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } end @@ -157,10 +157,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit if @merge_request.compare - @diffs = Compare.decorate(@merge_request.compare, @project).diff_file_collection( - diff_options: diff_options, - diff_refs: @merge_request.diff_refs - ) + @diffs = @merge_request.diff_file_collection(diff_options) end @diff_notes_disabled = true diff --git a/app/models/commit.rb b/app/models/commit.rb index d22ecb222e5..a339d47f5f3 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -317,7 +317,7 @@ class Commit nil end - def diff_file_collection(diff_options) + def diff_file_collection(diff_options = nil) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end diff --git a/app/models/compare.rb b/app/models/compare.rb index 6672d1bf059..05c8fbbcc36 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -1,5 +1,5 @@ class Compare - delegate :commits, :same, :head, :base, to: :@compare + delegate :same, :head, :base, to: :@compare def self.decorate(compare, project) if compare.is_a?(Compare) @@ -14,10 +14,53 @@ class Compare @project = project end - def diff_file_collection(diff_options:, diff_refs: nil) + def commits + @commits ||= Commit.decorate(@compare.commits, @project) + end + + def start_commit + return @start_commit if defined?(@start_commit) + + commit = @compare.base + @start_commit = commit ? ::Commit.new(commit, @project) : nil + end + + def commit + return @commit if defined?(@commit) + + commit = @compare.head + @commit = commit ? ::Commit.new(commit, @project) : nil + end + alias_method :head_commit, :commit + + # Used only on emails_on_push_worker.rb + def base_commit=(commit) + @base_commit = commit + end + + def base_commit + return @base_commit if defined?(@base_commit) + + @base_commit = if start_commit && commit + @project.merge_base_commit(start_commit.id, commit.id) + else + nil + end + end + + # keyword args until we get ride of diff_refs as argument + def diff_file_collection(diff_options:, diff_refs: self.diff_refs) Gitlab::Diff::FileCollection::Compare.new(@compare, project: @project, diff_options: diff_options, diff_refs: diff_refs) end + + def diff_refs + @diff_refs ||= Gitlab::Diff::DiffRefs.new( + base_sha: base_commit.try(:sha), + start_sha: start_commit.try(:sha), + head_sha: commit.try(:sha) + ) + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index abc8bacbe59..62e5573dfdc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -168,8 +168,12 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) end - def diff_file_collection(diff_options) - Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) + def diff_file_collection(diff_options = nil) + if self.compare + self.compare.diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) + else + Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) + end end def diff_size diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 149822aa647..bb3aff72b47 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -20,10 +20,13 @@ class CompareService ) end - Gitlab::Git::Compare.new( + raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, - source_sha, + source_sha ) + + # REVIEW be sure if it's target_project or source_project + Compare.new(raw_compare, target_project) end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 7fe57747265..290742f1506 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -34,7 +34,7 @@ module MergeRequests # At this point we decide if merge request can be created # If we have at least one commit to merge -> creation allowed if commits.present? - merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project) + merge_request.compare_commits = commits merge_request.can_be_created = true merge_request.compare = compare else diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb index 982540ba7f5..8151c24d1b0 100644 --- a/app/services/merge_requests/merge_request_diff_cache_service.rb +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -2,7 +2,7 @@ module MergeRequests class MergeRequestDiffCacheService def execute(merge_request) # Executing the iteration we cache all the highlighted diff information - merge_request.diff_file_collection(Gitlab::Diff::FileCollection.default_options).diff_files.to_a + merge_request.diff_file_collection.diff_files.to_a end end end diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 11b2020f99b..ed44d86a687 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -7,7 +7,7 @@ = render "ci_menu" - else %div.block-connector -= render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs += render "projects/diffs/diffs", diffs: @diffs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index eb8a1bd5289..819e9bc15ae 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs + = render "projects/diffs/diffs", diffs: @diffs - else .light-well .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 45895a9a3de..35fdf7d5278 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,4 +1,5 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) +- diff_files = diffs.diff_files - if diff_view == 'parallel' - fluid_layout true @@ -26,7 +27,7 @@ - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) - next unless blob - - blob.load_all_data!(project.repository) unless blob.only_display_raw? + - blob.load_all_data!(@project.repository) unless blob.only_display_raw? - = render 'projects/diffs/file', i: index, project: project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs + = render 'projects/diffs/file', i: index, project: @project, + diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diffs.diff_refs diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index cb2b623691c..598bd743676 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -42,7 +42,7 @@ %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %p To preserve performance the line changes are not shown. - else - = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false + = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false - if @pipeline #builds.builds.tab-pane = render "projects/merge_requests/show/builds" diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 5b842dd9280..c6d2567af35 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diff_files: @diffs.diff_files, + = render "projects/diffs/diffs", diffs: @diffs, diff_refs: @diffs.diff_refs, project: @diffs.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 0b6a01a3200..0b63913cfd1 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,25 +33,19 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha) + base_commit = project.merge_base_commit(before_sha, after_sha) compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - - diff_refs = Gitlab::Diff::DiffRefs.new( - base_sha: merge_base_sha, - start_sha: before_sha, - head_sha: after_sha - ) + compare = Compare.decorate(compare, project) + compare.base_commit = base_commit + diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) - - diff_refs = Gitlab::Diff::DiffRefs.new( - base_sha: merge_base_sha, - start_sha: after_sha, - head_sha: before_sha - ) + compare = Compare.decorate(compare, project) + compare.base_commit = base_commit + diff_refs = compare.diff_refs reverse_compare = true diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index 20562773c14..a0c88265c45 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -7,7 +7,7 @@ module Gitlab delegate :count, :size, :real_size, to: :diff_files - def initialize(diffs, project:, diff_options:, diff_refs: nil) + def initialize(diffs, project:, diff_options: nil, diff_refs: nil) @diffs = diffs @project = project @diff_options = diff_options diff --git a/lib/gitlab/diff/file_collection/commit.rb b/lib/gitlab/diff/file_collection/commit.rb index 2a46109ad99..19def300b74 100644 --- a/lib/gitlab/diff/file_collection/commit.rb +++ b/lib/gitlab/diff/file_collection/commit.rb @@ -3,6 +3,9 @@ module Gitlab module FileCollection class Commit < Base def initialize(commit, diff_options:) + # Not merge just set defaults + diff_options = diff_options || Gitlab::Diff::FileCollection.default_options + super(commit.diffs(diff_options), project: commit.project, diff_options: diff_options, diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb index 1bcda145f15..aba5a28b51f 100644 --- a/lib/gitlab/diff/file_collection/compare.rb +++ b/lib/gitlab/diff/file_collection/compare.rb @@ -3,6 +3,9 @@ module Gitlab module FileCollection class Compare < Base def initialize(compare, project:, diff_options:, diff_refs: nil) + # Not merge just set defaults + diff_options = diff_options || Gitlab::Diff::FileCollection.default_options + super(compare.diffs(diff_options), project: project, diff_options: diff_options, diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request.rb index 7c40622d594..9fde0bba183 100644 --- a/lib/gitlab/diff/file_collection/merge_request.rb +++ b/lib/gitlab/diff/file_collection/merge_request.rb @@ -4,6 +4,8 @@ module Gitlab class MergeRequest < Base def initialize(merge_request, diff_options:) @merge_request = merge_request + # Not merge just set defaults + diff_options = diff_options || Gitlab::Diff::FileCollection.default_options super(merge_request.diffs(diff_options), project: merge_request.project, @@ -27,15 +29,10 @@ module Gitlab if cacheable? cache_highlight!(diff_file) else - highlight_diff_file!(diff_file) + diff_file # Don't need to eager load highlighted diff lines end end - def highlight_diff_file!(diff_file) - diff_file.highlighted_diff_lines = Gitlab::Diff::Highlight.new(diff_file, repository: diff_file.repository).highlight - diff_file - end - def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) diff_file.highlighted_diff_lines = cache_diff_lines.map do |line| Gitlab::Diff::Line.init_from_hash(line) @@ -56,7 +53,6 @@ module Gitlab if highlight_cache[file_path] highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path]) else - highlight_diff_file!(diff_file) highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash) end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 71213813e17..16491ede71b 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -35,13 +35,13 @@ module Gitlab def commits return unless compare - @commits ||= Commit.decorate(compare.commits, project) + @commits ||= compare.commits end def diffs return unless compare - @diffs ||= compare.diff_file_collection(diff_options: { max_files: 30 }, diff_refs: diff_refs).diff_files + @diffs ||= compare.diff_file_collection(diff_options: { max_files: 30 }).diff_files end def diffs_count @@ -49,9 +49,7 @@ module Gitlab end def compare - if @opts[:compare] - Compare.decorate(@opts[:compare], project) - end + @opts[:compare] if @opts[:compare] end def diff_refs @@ -99,16 +97,18 @@ module Gitlab if commits.length > 1 namespace_project_compare_url(project_namespace, project, - from: Commit.new(compare.base, project), - to: Commit.new(compare.head, project)) + from: compare.start_commit, + to: compare.head_commit) else namespace_project_commit_url(project_namespace, - project, commits.first) + project, + commits.first) end else unless @action == :delete namespace_project_tree_url(project_namespace, - project, ref_name) + project, + ref_name) end end end diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index c1d07329983..5b966bddb6a 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -16,9 +16,12 @@ describe Gitlab::Email::Message::RepositoryPush do { author_id: author.id, ref: 'master', action: :push, compare: compare, send_from_committer_email: true } end - let(:compare) do + let(:raw_compare) do Gitlab::Git::Compare.new(project.repository.raw_repository, - sample_image_commit.id, sample_commit.id) + sample_image_commit.id, sample_commit.id) + end + let(:compare) do + Compare.decorate(raw_compare, project) end describe '#project' do @@ -62,7 +65,7 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#diffs_count' do subject { message.diffs_count } - it { is_expected.to eq compare.diffs.size } + it { is_expected.to eq raw_compare.diffs.size } end describe '#compare' do @@ -72,7 +75,7 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#compare_timeout' do subject { message.compare_timeout } - it { is_expected.to eq compare.diffs.overflow? } + it { is_expected.to eq raw_compare.diffs.overflow? } end describe '#reverse_compare?' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 3685b2b17b5..e2866ef160c 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -944,8 +944,9 @@ describe Notify do describe 'email on push with multiple commits' do let(:example_site_path) { root_path } let(:user) { create(:user) } - let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } - let(:commits) { Commit.decorate(compare.commits, nil) } + let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } + let(:compare) { Compare.decorate(raw_compare, project) } + let(:commits) { compare.commits } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:send_from_committer_email) { false } let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } @@ -1046,8 +1047,9 @@ describe Notify do describe 'email on push with a single commit' do let(:example_site_path) { root_path } let(:user) { create(:user) } - let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } - let(:commits) { Commit.decorate(compare.commits, nil) } + let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } + let(:compare) { Compare.decorate(raw_compare, project) } + let(:commits) { compare.commits } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 782d74ec5ec..232508cda23 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -61,7 +61,7 @@ describe MergeRequests::BuildService, services: true do end context 'one commit in the diff' do - let(:commits) { [commit_1] } + let(:commits) { Commit.decorate([commit_1], project) } it 'allows the merge request to be created' do expect(merge_request.can_be_created).to eq(true) @@ -84,7 +84,7 @@ describe MergeRequests::BuildService, services: true do end context 'commit has no description' do - let(:commits) { [commit_2] } + let(:commits) { Commit.decorate([commit_2], project) } it 'uses the title of the commit as the title of the merge request' do expect(merge_request.title).to eq(commit_2.safe_message) @@ -111,7 +111,7 @@ describe MergeRequests::BuildService, services: true do end context 'commit has no description' do - let(:commits) { [commit_2] } + let(:commits) { Commit.decorate([commit_2], project) } it 'sets the description to "Closes #$issue-iid"' do expect(merge_request.description).to eq("Closes ##{issue.iid}") @@ -121,7 +121,7 @@ describe MergeRequests::BuildService, services: true do end context 'more than one commit in the diff' do - let(:commits) { [commit_1, commit_2] } + let(:commits) { Commit.decorate([commit_1, commit_2], project) } it 'allows the merge request to be created' do expect(merge_request.can_be_created).to eq(true) From c86c1905b5574cac234315598d8d715fcaee3ea7 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 27 Jul 2016 19:00:34 +0200 Subject: [PATCH 190/198] switch from diff_file_collection to diffs So we have raw_diffs too --- app/controllers/projects/commit_controller.rb | 4 +- .../projects/compare_controller.rb | 4 +- .../projects/merge_requests_controller.rb | 8 +- app/helpers/diff_helper.rb | 5 +- app/models/commit.rb | 10 ++- app/models/compare.rb | 36 ++++----- app/models/legacy_diff_note.rb | 4 +- app/models/merge_request.rb | 8 +- app/models/repository.rb | 2 +- app/services/compare_service.rb | 1 - .../merge_request_diff_cache_service.rb | 2 +- app/views/projects/diffs/_diffs.html.haml | 14 ++-- .../merge_requests/show/_diffs.html.haml | 3 +- app/workers/emails_on_push_worker.rb | 9 +-- app/workers/irker_worker.rb | 6 +- lib/api/commits.rb | 4 +- lib/api/entities.rb | 2 +- lib/gitlab/diff/file_collection.rb | 9 --- lib/gitlab/diff/file_collection/base.rb | 17 ++-- lib/gitlab/diff/file_collection/commit.rb | 5 +- lib/gitlab/diff/file_collection/compare.rb | 5 +- .../diff/file_collection/merge_request.rb | 23 ++---- lib/gitlab/email/message/repository_push.rb | 3 +- spec/helpers/diff_helper_spec.rb | 24 +++--- spec/lib/gitlab/diff/file_spec.rb | 2 +- spec/lib/gitlab/diff/highlight_spec.rb | 2 +- spec/lib/gitlab/diff/line_mapper_spec.rb | 2 +- spec/lib/gitlab/diff/parallel_diff_spec.rb | 2 +- spec/lib/gitlab/diff/parser_spec.rb | 2 +- spec/models/compare_spec.rb | 77 +++++++++++++++++++ spec/models/legacy_diff_note_spec.rb | 2 +- spec/models/merge_request_spec.rb | 29 ++++++- spec/models/repository_spec.rb | 4 +- spec/requests/api/commits_spec.rb | 4 +- .../merge_request_diff_cache_service_spec.rb | 4 +- 35 files changed, 208 insertions(+), 130 deletions(-) delete mode 100644 lib/gitlab/diff/file_collection.rb create mode 100644 spec/models/compare_spec.rb diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 771a86530cd..fdfe7c65b7b 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -28,7 +28,7 @@ class Projects::CommitController < Projects::ApplicationController end def diff_for_path - render_diff_for_path(@commit.diff_file_collection(diff_options)) + render_diff_for_path(@commit.diffs(diff_options)) end def builds @@ -110,7 +110,7 @@ class Projects::CommitController < Projects::ApplicationController opts = diff_options opts[:ignore_whitespace_change] = true if params[:format] == 'diff' - @diffs = commit.diff_file_collection(opts) + @diffs = commit.diffs(opts) @notes_count = commit.notes.count end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 7fca5e77f32..4a42a7d091b 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(@compare.diff_file_collection(diff_options: diff_options)) + render_diff_for_path(@compare.diffs(diff_options: diff_options)) end def create @@ -45,7 +45,7 @@ class Projects::CompareController < Projects::ApplicationController @commit = @compare.commit @base_commit = @compare.base_commit - @diffs = @compare.diff_file_collection(diff_options: diff_options) + @diffs = @compare.diffs(diff_options: diff_options) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 20afc6afcb2..2cf6a2dd1b3 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -86,7 +86,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html { define_discussion_vars } format.json do - @diffs = @merge_request.diff_file_collection(diff_options) + @diffs = @merge_request.diffs(diff_options) render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } end @@ -108,7 +108,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController define_commit_vars - render_diff_for_path(@merge_request.diff_file_collection(diff_options)) + render_diff_for_path(@merge_request.diffs(diff_options)) end def commits @@ -156,9 +156,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.compare_commits.reverse @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit - if @merge_request.compare - @diffs = @merge_request.diff_file_collection(diff_options) - end + @diffs = @merge_request.diffs(diff_options) if @merge_request.compare @diff_notes_disabled = true @pipeline = @merge_request.pipeline diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 2abe24b78bf..cc7121b1163 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -23,10 +23,7 @@ module DiffHelper end def diff_options - options = Gitlab::Diff::FileCollection.default_options.merge( - ignore_whitespace_change: hide_whitespace?, - no_collapse: expand_all_diffs? - ) + options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? } if action_name == 'diff_for_path' options[:no_collapse] = true diff --git a/app/models/commit.rb b/app/models/commit.rb index a339d47f5f3..d58c2fb8106 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -104,7 +104,7 @@ class Commit end def diff_line_count - @diff_line_count ||= Commit::diff_line_count(self.diffs) + @diff_line_count ||= Commit::diff_line_count(raw_diffs) @diff_line_count end @@ -317,7 +317,11 @@ class Commit nil end - def diff_file_collection(diff_options = nil) + def raw_diffs(*args) + raw.diffs(*args) + end + + def diffs(diff_options = nil) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end @@ -330,7 +334,7 @@ class Commit def repo_changes changes = { added: [], modified: [], removed: [] } - diffs.each do |diff| + raw_diffs.each do |diff| if diff.deleted_file changes[:removed] << diff.old_path elsif diff.renamed_file || diff.new_file diff --git a/app/models/compare.rb b/app/models/compare.rb index 05c8fbbcc36..98c042f3809 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -1,6 +1,8 @@ class Compare delegate :same, :head, :base, to: :@compare + attr_reader :project + def self.decorate(compare, project) if compare.is_a?(Compare) compare @@ -15,49 +17,47 @@ class Compare end def commits - @commits ||= Commit.decorate(@compare.commits, @project) + @commits ||= Commit.decorate(@compare.commits, project) end def start_commit return @start_commit if defined?(@start_commit) commit = @compare.base - @start_commit = commit ? ::Commit.new(commit, @project) : nil + @start_commit = commit ? ::Commit.new(commit, project) : nil end - def commit - return @commit if defined?(@commit) + def head_commit + return @head_commit if defined?(@head_commit) commit = @compare.head - @commit = commit ? ::Commit.new(commit, @project) : nil - end - alias_method :head_commit, :commit - - # Used only on emails_on_push_worker.rb - def base_commit=(commit) - @base_commit = commit + @head_commit = commit ? ::Commit.new(commit, project) : nil end + alias_method :commit, :head_commit def base_commit return @base_commit if defined?(@base_commit) - @base_commit = if start_commit && commit - @project.merge_base_commit(start_commit.id, commit.id) + @base_commit = if start_commit && head_commit + project.merge_base_commit(start_commit.id, head_commit.id) else nil end end - # keyword args until we get ride of diff_refs as argument - def diff_file_collection(diff_options:, diff_refs: self.diff_refs) - Gitlab::Diff::FileCollection::Compare.new(@compare, - project: @project, + def raw_diffs(*args) + @compare.diffs(*args) + end + + def diffs(diff_options:) + Gitlab::Diff::FileCollection::Compare.new(self, + project: project, diff_options: diff_options, diff_refs: diff_refs) end def diff_refs - @diff_refs ||= Gitlab::Diff::DiffRefs.new( + Gitlab::Diff::DiffRefs.new( base_sha: base_commit.try(:sha), start_sha: start_commit.try(:sha), head_sha: commit.try(:sha) diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 865712268a0..6ed66001513 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -85,7 +85,7 @@ class LegacyDiffNote < Note return nil unless noteable return @diff if defined?(@diff) - @diff = noteable.diffs(Commit.max_diff_options).find do |d| + @diff = noteable.raw_diffs(Commit.max_diff_options).find do |d| d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash end end @@ -116,7 +116,7 @@ class LegacyDiffNote < Note # Find the diff on noteable that matches our own def find_noteable_diff - diffs = noteable.diffs(Commit.max_diff_options) + diffs = noteable.raw_diffs(Commit.max_diff_options) diffs.find { |d| d.new_path == self.diff.new_path } end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 62e5573dfdc..009262d6b48 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -164,13 +164,13 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end - def diffs(*args) - merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) + def raw_diffs(*args) + merge_request_diff ? merge_request_diff.diffs(*args) : compare.raw_diffs(*args) end - def diff_file_collection(diff_options = nil) + def diffs(diff_options = nil) if self.compare - self.compare.diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) + self.compare.diffs(diff_options: diff_options) else Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) end diff --git a/app/models/repository.rb b/app/models/repository.rb index bac37483c47..3d95344a68f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -372,7 +372,7 @@ class Repository # We don't want to flush the cache if the commit didn't actually make any # changes to any of the possible avatar files. if revision && commit = self.commit(revision) - return unless commit.diffs. + return unless commit.raw_diffs. any? { |diff| AVATAR_FILES.include?(diff.new_path) } end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index bb3aff72b47..6d6075628af 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -26,7 +26,6 @@ class CompareService source_sha ) - # REVIEW be sure if it's target_project or source_project Compare.new(raw_compare, target_project) end end diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb index 8151c24d1b0..2945a7fd4e4 100644 --- a/app/services/merge_requests/merge_request_diff_cache_service.rb +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -2,7 +2,7 @@ module MergeRequests class MergeRequestDiffCacheService def execute(merge_request) # Executing the iteration we cache all the highlighted diff information - merge_request.diff_file_collection.diff_files.to_a + merge_request.diffs.diff_files.to_a end end end diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 35fdf7d5278..20dc280c3b2 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -9,11 +9,11 @@ = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle - if current_controller?(:commit) - = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') + = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') - elsif current_controller?(:merge_requests) - = diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs') + = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs') - elsif current_controller?(:compare) - = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs') + = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs') .btn-group = inline_diff_btn = parallel_diff_btn @@ -22,12 +22,12 @@ - if diff_files.overflow? = render 'projects/diffs/warning', diff_files: diff_files -.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, @project))}} +.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}} - diff_files.each_with_index do |diff_file, index| - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) - next unless blob - - blob.load_all_data!(@project.repository) unless blob.only_display_raw? + - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw? - = render 'projects/diffs/file', i: index, project: @project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diffs.diff_refs + = render 'projects/diffs/file', i: index, project: diffs.project, + diff_file: diff_file, diff_commit: diff_commit, blob: blob diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index c6d2567af35..013b05628fa 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,6 +1,5 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: @diffs, - diff_refs: @diffs.diff_refs, project: @diffs.project + = render "projects/diffs/diffs", diffs: @diffs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 0b63913cfd1..c6a5af2809a 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,18 +33,13 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - base_commit = project.merge_base_commit(before_sha, after_sha) - compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - compare = Compare.decorate(compare, project) - compare.base_commit = base_commit + compare = CompareService.new.execute(project, before_sha, project, after_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) - compare = Compare.decorate(compare, project) - compare.base_commit = base_commit + compare = CompareService.new.execute(project, after_sha, project, before_sha) diff_refs = compare.diff_refs reverse_compare = true diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index a3c34e02baa..07cc7c1cbd7 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -141,8 +141,10 @@ class IrkerWorker end def files_count(commit) - files = "#{commit.diffs.real_size} file" - files += 's' if commit.diffs.size > 1 + diffs = commit.raw_diffs + + files = "#{diffs.real_size} file" + files += 's' if diffs.size > 1 files end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 4a11c8e3620..b4eaf1813d4 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -54,7 +54,7 @@ module API sha = params[:sha] commit = user_project.commit(sha) not_found! "Commit" unless commit - commit.diffs.to_a + commit.raw_diffs.to_a end # Get a commit's comments @@ -96,7 +96,7 @@ module API } if params[:path] && params[:line] && params[:line_type] - commit.diffs(all_diffs: true).each do |diff| + commit.raw_diffs(all_diffs: true).each do |diff| next unless diff.new_path == params[:path] lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3e21b7a0b8a..e5b00dc45a5 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -224,7 +224,7 @@ module API class MergeRequestChanges < MergeRequest expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _| - compare.diffs(all_diffs: true).to_a + compare.raw_diffs(all_diffs: true).to_a end end diff --git a/lib/gitlab/diff/file_collection.rb b/lib/gitlab/diff/file_collection.rb deleted file mode 100644 index ce6717c7205..00000000000 --- a/lib/gitlab/diff/file_collection.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module Diff - module FileCollection - def self.default_options - ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) - end - end - end -end diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index a0c88265c45..2b9fc65b985 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -1,28 +1,33 @@ module Gitlab module Diff module FileCollection - class Base attr_reader :project, :diff_options, :diff_view, :diff_refs delegate :count, :size, :real_size, to: :diff_files - def initialize(diffs, project:, diff_options: nil, diff_refs: nil) - @diffs = diffs + def self.default_options + ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) + end + + def initialize(diffable, project:, diff_options: nil, diff_refs: nil) + diff_options = self.class.default_options.merge(diff_options || {}) + + @diffable = diffable + @diffs = diffable.raw_diffs(diff_options) @project = project @diff_options = diff_options @diff_refs = diff_refs end def diff_files - @diffs.decorate! { |diff| decorate_diff!(diff) } + @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } end private def decorate_diff!(diff) - return diff if diff.is_a?(Gitlab::Diff::File) - Gitlab::Diff::File.new(diff, diff_refs: @diff_refs, repository: @project.repository) + Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs) end end end diff --git a/lib/gitlab/diff/file_collection/commit.rb b/lib/gitlab/diff/file_collection/commit.rb index 19def300b74..4dc297ec036 100644 --- a/lib/gitlab/diff/file_collection/commit.rb +++ b/lib/gitlab/diff/file_collection/commit.rb @@ -3,10 +3,7 @@ module Gitlab module FileCollection class Commit < Base def initialize(commit, diff_options:) - # Not merge just set defaults - diff_options = diff_options || Gitlab::Diff::FileCollection.default_options - - super(commit.diffs(diff_options), + super(commit, project: commit.project, diff_options: diff_options, diff_refs: commit.diff_refs) diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb index aba5a28b51f..20d8f891cc3 100644 --- a/lib/gitlab/diff/file_collection/compare.rb +++ b/lib/gitlab/diff/file_collection/compare.rb @@ -3,10 +3,7 @@ module Gitlab module FileCollection class Compare < Base def initialize(compare, project:, diff_options:, diff_refs: nil) - # Not merge just set defaults - diff_options = diff_options || Gitlab::Diff::FileCollection.default_options - - super(compare.diffs(diff_options), + super(compare, project: project, diff_options: diff_options, diff_refs: diff_refs) diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request.rb index 9fde0bba183..4f946908e2f 100644 --- a/lib/gitlab/diff/file_collection/merge_request.rb +++ b/lib/gitlab/diff/file_collection/merge_request.rb @@ -4,10 +4,8 @@ module Gitlab class MergeRequest < Base def initialize(merge_request, diff_options:) @merge_request = merge_request - # Not merge just set defaults - diff_options = diff_options || Gitlab::Diff::FileCollection.default_options - super(merge_request.diffs(diff_options), + super(merge_request, project: merge_request.project, diff_options: diff_options, diff_refs: merge_request.diff_refs) @@ -19,18 +17,11 @@ module Gitlab private - # Extracted method to highlight in the same iteration to the diff_collection. Iteration in the DiffCollections - # seems particularly slow on big diffs (event when already populated). + # Extracted method to highlight in the same iteration to the diff_collection. def decorate_diff!(diff) - highlight! super - end - - def highlight!(diff_file) - if cacheable? - cache_highlight!(diff_file) - else - diff_file # Don't need to eager load highlighted diff lines - end + diff_file = super + cache_highlight!(diff_file) if cacheable? + diff_file end def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) @@ -55,15 +46,13 @@ module Gitlab else highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash) end - - diff_file end def highlight_cache return @highlight_cache if defined?(@highlight_cache) @highlight_cache = Rails.cache.read(cache_key) || {} - @highlight_cache_was_empty = highlight_cache.empty? + @highlight_cache_was_empty = @highlight_cache.empty? @highlight_cache end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 16491ede71b..62d29387d60 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -41,7 +41,8 @@ module Gitlab def diffs return unless compare - @diffs ||= compare.diff_file_collection(diff_options: { max_files: 30 }).diff_files + # This diff is more moderated in number of files and lines + @diffs ||= compare.diffs(diff_options: { max_files: 30, max_lines: 5000, no_collapse: true }).diff_files end def diffs_count diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index c2fd2c8a533..4949280d641 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -6,7 +6,7 @@ describe DiffHelper do let(:project) { create(:project) } let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } - let(:diffs) { commit.diffs } + let(:diffs) { commit.raw_diffs } let(:diff) { diffs.first } let(:diff_refs) { [commit.parent, commit] } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } @@ -32,16 +32,6 @@ describe DiffHelper do end describe 'diff_options' do - it 'should return hard limit for a diff if force diff is true' do - allow(controller).to receive(:params) { { force_show_diff: true } } - expect(diff_options).to include(Commit.max_diff_options) - end - - it 'should return hard limit for a diff if expand_all_diffs is true' do - allow(controller).to receive(:params) { { expand_all_diffs: true } } - expect(diff_options).to include(Commit.max_diff_options) - end - it 'should return no collapse false' do expect(diff_options).to include(no_collapse: false) end @@ -55,6 +45,18 @@ describe DiffHelper do allow(controller).to receive(:action_name) { 'diff_for_path' } expect(diff_options).to include(no_collapse: true) end + + it 'should return paths if action name diff_for_path and param old path' do + allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } } + allow(controller).to receive(:action_name) { 'diff_for_path' } + expect(diff_options[:paths]).to include('lib/wadus.rb') + end + + it 'should return paths if action name diff_for_path and param new path' do + allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } } + allow(controller).to receive(:action_name) { 'diff_for_path' } + expect(diff_options[:paths]).to include('lib/wadus.rb') + end end describe 'unfold_bottom_class' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index e883a6eb9c2..0650cb291e5 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } - let(:diff) { commit.diffs.first } + let(:diff) { commit.raw_diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe '#diff_lines' do diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 88e4115c453..1c2ddeed692 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Diff::Highlight, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } - let(:diff) { commit.diffs.first } + let(:diff) { commit.raw_diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe '#highlight' do diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb index 4e50e03bb7e..4b943fa382d 100644 --- a/spec/lib/gitlab/diff/line_mapper_spec.rb +++ b/spec/lib/gitlab/diff/line_mapper_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::LineMapper, lib: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } - let(:diffs) { commit.diffs } + let(:diffs) { commit.raw_diffs } let(:diff) { diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } subject { described_class.new(diff_file) } diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index 2aa5ae44f54..af18d3c25a6 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } - let(:diffs) { commit.diffs } + let(:diffs) { commit.raw_diffs } let(:diff) { diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } subject { described_class.new(diff_file) } diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index c3359627652..b983d73f8be 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Diff::Parser, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } - let(:diff) { commit.diffs.first } + let(:diff) { commit.raw_diffs.first } let(:parser) { Gitlab::Diff::Parser.new } describe '#parse' do diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb new file mode 100644 index 00000000000..49ab3c4b6e9 --- /dev/null +++ b/spec/models/compare_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Compare, models: true do + include RepoHelpers + + let(:project) { create(:project, :public) } + let(:commit) { project.commit } + + let(:start_commit) { sample_image_commit } + let(:head_commit) { sample_commit } + + let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, start_commit.id, head_commit.id) } + + subject { described_class.new(raw_compare, project) } + + describe '#start_commit' do + it 'returns raw compare base commit' do + expect(subject.start_commit.id).to eq(start_commit.id) + end + + it 'returns nil if compare base commit is nil' do + expect(raw_compare).to receive(:base).and_return(nil) + + expect(subject.start_commit).to eq(nil) + end + end + + describe '#commit' do + it 'returns raw compare head commit' do + expect(subject.commit.id).to eq(head_commit.id) + end + + it 'returns nil if compare head commit is nil' do + expect(raw_compare).to receive(:head).and_return(nil) + + expect(subject.commit).to eq(nil) + end + end + + describe '#base_commit' do + let(:base_commit) { Commit.new(another_sample_commit, project) } + + it 'returns project merge base commit' do + expect(project).to receive(:merge_base_commit).with(start_commit.id, head_commit.id).and_return(base_commit) + + expect(subject.base_commit).to eq(base_commit) + end + + it 'returns nil if there is no start_commit' do + expect(subject).to receive(:start_commit).and_return(nil) + + expect(subject.base_commit).to eq(nil) + end + + it 'returns nil if there is no head commit' do + expect(subject).to receive(:head_commit).and_return(nil) + + expect(subject.base_commit).to eq(nil) + end + end + + describe '#diff_refs' do + it 'uses base_commit sha as base_sha' do + expect(subject).to receive(:base_commit).at_least(:once).and_call_original + + expect(subject.diff_refs.base_sha).to eq(subject.base_commit.id) + end + + it 'uses start_commit sha as start_sha' do + expect(subject.diff_refs.start_sha).to eq(start_commit.id) + end + + it 'uses commit sha as head sha' do + expect(subject.diff_refs.head_sha).to eq(head_commit.id) + end + end +end diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb index d23fc06c3ad..c8ee656fe3b 100644 --- a/spec/models/legacy_diff_note_spec.rb +++ b/spec/models/legacy_diff_note_spec.rb @@ -58,7 +58,7 @@ describe LegacyDiffNote, models: true do # Generate a real line_code value so we know it will match. We use a # random line from a random diff just for funsies. - diff = merge.diffs.to_a.sample + diff = merge.raw_diffs.to_a.sample line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fa1f7edae8e..152e0cce5ad 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -128,7 +128,7 @@ describe MergeRequest, models: true do end end - describe '#diffs' do + describe '#raw_diffs' do let(:merge_request) { build(:merge_request) } let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } } @@ -138,6 +138,31 @@ describe MergeRequest, models: true do expect(merge_request.merge_request_diff).to receive(:diffs).with(options) + merge_request.raw_diffs(options) + end + end + + context 'when there are no MR diffs' do + it 'delegates to the compare object' do + merge_request.compare = double(:compare) + + expect(merge_request.compare).to receive(:raw_diffs).with(options) + + merge_request.raw_diffs(options) + end + end + end + + describe '#diffs' do + let(:merge_request) { build(:merge_request) } + let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } } + + context 'when there are MR diffs' do + it 'delegates to the MR diffs' do + merge_request.merge_request_diff = MergeRequestDiff.new + + expect(merge_request.merge_request_diff).to receive(:diffs).with(hash_including(options)) + merge_request.diffs(options) end end @@ -146,7 +171,7 @@ describe MergeRequest, models: true do it 'delegates to the compare object' do merge_request.compare = double(:compare) - expect(merge_request.compare).to receive(:diffs).with(options) + expect(merge_request.compare).to receive(:diffs).with(diff_options: options) merge_request.diffs(options) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index cce15538b93..2a053b1804f 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1154,7 +1154,7 @@ describe Repository, models: true do it 'does not flush the cache if the commit does not change any logos' do diff = double(:diff, new_path: 'test.txt') - expect(commit).to receive(:diffs).and_return([diff]) + expect(commit).to receive(:raw_diffs).and_return([diff]) expect(cache).not_to receive(:expire) repository.expire_avatar_cache(repository.root_ref, '123') @@ -1163,7 +1163,7 @@ describe Repository, models: true do it 'flushes the cache if the commit changes any of the logos' do diff = double(:diff, new_path: Repository::AVATAR_FILES[0]) - expect(commit).to receive(:diffs).and_return([diff]) + expect(commit).to receive(:raw_diffs).and_return([diff]) expect(cache).to receive(:expire).with(:avatar) repository.expire_avatar_cache(repository.root_ref, '123') diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index e4ea8506598..51ee2167d47 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -173,10 +173,10 @@ describe API::API, api: true do end it 'should return the inline comment' do - post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new' + post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 7, line_type: 'new' expect(response).to have_http_status(201) expect(json_response['note']).to eq('My comment') - expect(json_response['path']).to eq(project.repository.commit.diffs.first.new_path) + expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path) expect(json_response['line']).to eq(7) expect(json_response['line_type']).to eq('new') end diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index c6cceed31ad..8f71d71b0f0 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -5,9 +5,9 @@ describe MergeRequests::MergeRequestDiffCacheService do let(:subject) { MergeRequests::MergeRequestDiffCacheService.new } describe '#execute' do - it 'retrieve the diff files to cache the highlighted result' do + it 'retrieves the diff files to cache the highlighted result' do merge_request = create(:merge_request) - cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection.default_options] + cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequest.default_options] expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) From 1a9431b914b7d138a37557b5d4a81ecd2423692c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 3 Aug 2016 08:54:01 +0300 Subject: [PATCH 191/198] Move markdown doc to the right location [ci skip] --- doc/README.md | 2 +- doc/markdown/markdown.md | 1 + doc/user/{project => }/img/markdown_logo.png | Bin doc/user/{project => }/img/markdown_video.mp4 | Bin doc/user/{project => }/markdown.md | 0 5 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/markdown/markdown.md rename doc/user/{project => }/img/markdown_logo.png (100%) rename doc/user/{project => }/img/markdown_video.mp4 (100%) rename doc/user/{project => }/markdown.md (100%) diff --git a/doc/README.md b/doc/README.md index 751e685b19b..d28ad499d3a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -9,7 +9,7 @@ - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). - [Importing and exporting projects between instances](user/project/settings/import_export.md). -- [Markdown](user/project/markdown.md) GitLab's advanced formatting system. +- [Markdown](user/markdown.md) GitLab's advanced formatting system. - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md new file mode 100644 index 00000000000..4ac81ab3ee7 --- /dev/null +++ b/doc/markdown/markdown.md @@ -0,0 +1 @@ +This document was moved to [user/markdown.md](../user/markdown.md). diff --git a/doc/user/project/img/markdown_logo.png b/doc/user/img/markdown_logo.png similarity index 100% rename from doc/user/project/img/markdown_logo.png rename to doc/user/img/markdown_logo.png diff --git a/doc/user/project/img/markdown_video.mp4 b/doc/user/img/markdown_video.mp4 similarity index 100% rename from doc/user/project/img/markdown_video.mp4 rename to doc/user/img/markdown_video.mp4 diff --git a/doc/user/project/markdown.md b/doc/user/markdown.md similarity index 100% rename from doc/user/project/markdown.md rename to doc/user/markdown.md From c91168c04a71301d8423b881c1219cfd510d5784 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 3 Aug 2016 17:33:37 +0300 Subject: [PATCH 192/198] Small refactor on Registry troubleshooting [ci skip] --- doc/container_registry/troubleshooting.md | 49 +++++++++++------------ 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md index 8008bf29935..14c4a7d9a63 100644 --- a/doc/container_registry/troubleshooting.md +++ b/doc/container_registry/troubleshooting.md @@ -5,27 +5,27 @@ 1. Check to make sure that the system clock on your Docker client and GitLab server have been synchronized (e.g. via NTP). -2. If you are using an S3-backed registry, double check that the IAM +2. If you are using an S3-backed Registry, double check that the IAM permissions and the S3 credentials (including region) are correct. See [the sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/) for more details. -3. Check the registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs +3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues there. -# Advanced Troubleshooting +## Advanced Troubleshooting -NOTE: The following section is only recommended for experts. +>**NOTE:** The following section is only recommended for experts. Sometimes it's not obvious what is wrong, and you may need to dive deeper into -the communication between the Docker client and the registry to find out +the communication between the Docker client and the Registry to find out what's wrong. We will use a concrete example in the past to illustrate how to diagnose a problem with the S3 setup. -## Example: Unexpected 403 error during push +### Unexpected 403 error during push -A user attempted to enable an S3-backed registry. The `docker login` step went +A user attempted to enable an S3-backed Registry. The `docker login` step went fine. However, when pushing an image, the output showed: ``` @@ -44,11 +44,11 @@ error parsing HTTP 403 response body: unexpected end of JSON input: "" ``` This error is ambiguous, as it's not clear whether the 403 is coming from the -GitLab Rails application, the Docker registry, or something else. In this +GitLab Rails application, the Docker Registry, or something else. In this case, since we know that since the login succeeded, we probably need to look -at the communication between the client and the registry. +at the communication between the client and the Registry. -The REST API between the Docker client and registry is [described +The REST API between the Docker client and Registry is [described here](https://docs.docker.com/registry/spec/api/). Normally, one would just use Wireshark or tcpdump to capture the traffic and see where things went wrong. However, since all communication between Docker clients and servers @@ -56,12 +56,12 @@ are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even if you know the private key. What can we do instead? One way would be to disable HTTPS by setting up an [insecure -registry](https://docs.docker.com/registry/insecure/). This could introduce a +Registry](https://docs.docker.com/registry/insecure/). This could introduce a security hole and is only recommended for local testing. If you have a production system and can't or don't want to do this, there is another way: use mitmproxy, which stands for Man-in-the-Middle Proxy. -## mitmproxy +### mitmproxy [mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your client and server to inspect all traffic. One wrinkle is that your system @@ -70,10 +70,9 @@ needs to trust the mitmproxy SSL certificates for this to work. The following installation instructions assume you are running Ubuntu: 1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html) - -2. Run `mitmproxy --port 9000` to generate its certificates. Enter CTRL-C to quit. - -3. Install the certificate from ~/.mitmproxy to your system: +1. Run `mitmproxy --port 9000` to generate its certificates. + Enter CTRL-C to quit. +1. Install the certificate from `~/.mitmproxy` to your system: ```sh sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt @@ -87,24 +86,22 @@ Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d....done. ``` -## Verifying mitmproxy certifiactes - -To verify that the certificates are properly install, run: +To verify that the certificates are properly installed, run: ```sh mitmproxy --port 9000 ``` -This will run mitmproxy on port 9000. In another window, run: +This will run mitmproxy on port `9000`. In another window, run: ```sh curl --proxy http://localhost:9000 https://httpbin.org/status/200 ``` -If everything is setup correctly, then you will see information on the mitmproxy window and +If everything is setup correctly, you will see information on the mitmproxy window and no errors from the curl commands. -## Running the Docker daemon with a proxy +### Running the Docker daemon with a proxy For Docker to connect through a proxy, you must start the Docker daemon with the proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`) @@ -118,10 +115,10 @@ docker daemon --debug This will launch the Docker daemon and proxy all connections through mitmproxy. -## Running the Docker client +### Running the Docker client -Now that we have mitmproxy and Docker running, we can now attempt to login and push a container -image. You may need to run as root to do this. For example: +Now that we have mitmproxy and Docker running, we can attempt to login and push +a container image. You may need to run as root to do this. For example: ```sh docker login s3-testing.myregistry.com:4567 @@ -141,4 +138,4 @@ The above image shows: What does this mean? This strongly suggests that the S3 user does not have the right [permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html). The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/). -Once the right permissions were set, the error went away. +Once the right permissions were set, the error will go away. From c008a1a9674f7c01b4504e22ed414b07eff05385 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 3 Aug 2016 09:32:01 -0700 Subject: [PATCH 193/198] Make Compare#diffs diff_options a regular argument --- app/controllers/projects/compare_controller.rb | 4 ++-- app/models/compare.rb | 2 +- app/models/merge_request.rb | 2 +- lib/gitlab/email/message/repository_push.rb | 2 +- spec/models/merge_request_spec.rb | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 4a42a7d091b..bee3d56076c 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(@compare.diffs(diff_options: diff_options)) + render_diff_for_path(@compare.diffs(diff_options)) end def create @@ -45,7 +45,7 @@ class Projects::CompareController < Projects::ApplicationController @commit = @compare.commit @base_commit = @compare.base_commit - @diffs = @compare.diffs(diff_options: diff_options) + @diffs = @compare.diffs(diff_options) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/models/compare.rb b/app/models/compare.rb index 98c042f3809..4856510f526 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -49,7 +49,7 @@ class Compare @compare.diffs(*args) end - def diffs(diff_options:) + def diffs(diff_options = nil) Gitlab::Diff::FileCollection::Compare.new(self, project: project, diff_options: diff_options, diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 009262d6b48..c4761fac2fb 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -170,7 +170,7 @@ class MergeRequest < ActiveRecord::Base def diffs(diff_options = nil) if self.compare - self.compare.diffs(diff_options: diff_options) + self.compare.diffs(diff_options) else Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 62d29387d60..0e3b65fceb4 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -42,7 +42,7 @@ module Gitlab return unless compare # This diff is more moderated in number of files and lines - @diffs ||= compare.diffs(diff_options: { max_files: 30, max_lines: 5000, no_collapse: true }).diff_files + @diffs ||= compare.diffs(max_files: 30, max_lines: 5000, no_collapse: true).diff_files end def diffs_count diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 152e0cce5ad..e43008c1a4d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -171,7 +171,7 @@ describe MergeRequest, models: true do it 'delegates to the compare object' do merge_request.compare = double(:compare) - expect(merge_request.compare).to receive(:diffs).with(diff_options: options) + expect(merge_request.compare).to receive(:diffs).with(options) merge_request.diffs(options) end From a16c26c957ae893f6957fd0ad66c189d0b8ca079 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Fri, 29 Jul 2016 19:31:37 +0200 Subject: [PATCH 194/198] Speed up Commit#repo_changes --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- app/models/commit.rb | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 38e91fc3e98..8d0e377e31a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,6 +57,7 @@ v 8.11.0 (unreleased) - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) - Fix RequestProfiler::Middleware error when code is reloaded in development - Catch what warden might throw when profiling requests to re-throw it + - Speed up Commit#repo_changes v 8.10.3 - Fix Import/Export issue importing milestones and labels not associated properly. !5426 diff --git a/Gemfile b/Gemfile index 5f247abd2fc..16f24553ed1 100644 --- a/Gemfile +++ b/Gemfile @@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.4.2' +gem 'gitlab_git', '~> 10.4.3' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 7b4175ea824..866f5014847 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.4.2) + gitlab_git (10.4.3) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -870,7 +870,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.4.2) + gitlab_git (~> 10.4.3) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) diff --git a/app/models/commit.rb b/app/models/commit.rb index d58c2fb8106..cc413448ce8 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -334,7 +334,7 @@ class Commit def repo_changes changes = { added: [], modified: [], removed: [] } - raw_diffs.each do |diff| + raw_diffs(deltas_only: true).each do |diff| if diff.deleted_file changes[:removed] << diff.old_path elsif diff.renamed_file || diff.new_file From 6eba7188f1cd1fc0bfcb8b1cf46f40338dc892b5 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 29 Jul 2016 13:46:39 -0700 Subject: [PATCH 195/198] Use only deltas in diffs when scanning the last commit for changes in the avatar to save memory --- CHANGELOG | 2 +- app/models/repository.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8d0e377e31a..864b8afaf7d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,7 +57,7 @@ v 8.11.0 (unreleased) - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) - Fix RequestProfiler::Middleware error when code is reloaded in development - Catch what warden might throw when profiling requests to re-throw it - - Speed up Commit#repo_changes + - Speed up and reduce memory usage of Commit#repo_changes and Repository#expire_avatar_cache v 8.10.3 - Fix Import/Export issue importing milestones and labels not associated properly. !5426 diff --git a/app/models/repository.rb b/app/models/repository.rb index 3d95344a68f..c1170c470ea 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -372,7 +372,7 @@ class Repository # We don't want to flush the cache if the commit didn't actually make any # changes to any of the possible avatar files. if revision && commit = self.commit(revision) - return unless commit.raw_diffs. + return unless commit.raw_diffs(deltas_only: true). any? { |diff| AVATAR_FILES.include?(diff.new_path) } end From 08c1dd348273df67bf14172e9082308e12f94784 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Mon, 1 Aug 2016 13:14:41 +0200 Subject: [PATCH 196/198] Use commit deltas when counting files in IrkerWorker --- CHANGELOG | 2 +- app/workers/irker_worker.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 864b8afaf7d..25911e02ec6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,7 +57,7 @@ v 8.11.0 (unreleased) - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) - Fix RequestProfiler::Middleware error when code is reloaded in development - Catch what warden might throw when profiling requests to re-throw it - - Speed up and reduce memory usage of Commit#repo_changes and Repository#expire_avatar_cache + - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker v 8.10.3 - Fix Import/Export issue importing milestones and labels not associated properly. !5426 diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 07cc7c1cbd7..19f38358eb5 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -141,7 +141,7 @@ class IrkerWorker end def files_count(commit) - diffs = commit.raw_diffs + diffs = commit.raw_diffs(deltas_only: true) files = "#{diffs.real_size} file" files += 's' if diffs.size > 1 From 8a62f4e7d46908c56bedf155792323dc4218be94 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 3 Aug 2016 11:22:40 -0700 Subject: [PATCH 197/198] Update the gitlab-shell version in the tmp/tests directory to the right version Previously the gitlab-shell version would never be updated if the directory existed via the `gitlab:shell:install` Rake task. This could lead to incompatibility issues or random errors. --- lib/tasks/gitlab/shell.rake | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index c85ebdf8619..ba93945bd03 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -5,7 +5,8 @@ namespace :gitlab do warn_user_is_not_gitlab default_version = Gitlab::Shell.version_required - args.with_defaults(tag: 'v' + default_version, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git") + default_version_tag = 'v' + default_version + args.with_defaults(tag: default_version_tag, repo: "https://gitlab.com/gitlab-org/gitlab-shell.git") user = Gitlab.config.gitlab.user home_dir = Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home @@ -15,7 +16,12 @@ namespace :gitlab do target_dir = Gitlab.config.gitlab_shell.path # Clone if needed - unless File.directory?(target_dir) + if File.directory?(target_dir) + Dir.chdir(target_dir) do + system(*%W(Gitlab.config.git.bin_path} fetch --tags --quiet)) + system(*%W(Gitlab.config.git.bin_path} checkout --quiet #{default_version_tag})) + end + else system(*%W(#{Gitlab.config.git.bin_path} clone -- #{args.repo} #{target_dir})) end From 631f59d4e7f449c59735cd7eab25cf407e06d5d8 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 3 Aug 2016 23:32:12 +0200 Subject: [PATCH 198/198] change the API on the merge_request_diff model from diffs -> raw_diffs --- app/models/merge_request.rb | 2 +- app/models/merge_request_diff.rb | 10 +++++----- spec/models/merge_request_diff_spec.rb | 12 ++++++------ spec/models/merge_request_spec.rb | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c4761fac2fb..b1fb3ce5d69 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -165,7 +165,7 @@ class MergeRequest < ActiveRecord::Base end def raw_diffs(*args) - merge_request_diff ? merge_request_diff.diffs(*args) : compare.raw_diffs(*args) + merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args) end def diffs(diff_options = nil) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 119266f2d2c..fa0efe2d596 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -33,12 +33,12 @@ class MergeRequestDiff < ActiveRecord::Base end def size - real_size.presence || diffs.size + real_size.presence || raw_diffs.size end - def diffs(options={}) + def raw_diffs(options={}) if options[:ignore_whitespace_change] - @diffs_no_whitespace ||= begin + @raw_diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( repository.raw_repository, self.start_commit_sha || self.target_branch_sha, @@ -47,8 +47,8 @@ class MergeRequestDiff < ActiveRecord::Base compare.diffs(options) end else - @diffs ||= {} - @diffs[options] ||= load_diffs(st_diffs, options) + @raw_diffs ||= {} + @raw_diffs[options] ||= load_diffs(st_diffs, options) end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 9a637c94fbe..29f7396f862 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -10,7 +10,7 @@ describe MergeRequestDiff, models: true do expect(mr_diff).not_to receive(:load_diffs) expect(Gitlab::Git::Compare).to receive(:new).and_call_original - mr_diff.diffs(ignore_whitespace_change: true) + mr_diff.raw_diffs(ignore_whitespace_change: true) end end @@ -18,19 +18,19 @@ describe MergeRequestDiff, models: true do before { mr_diff.update_attributes(st_diffs: '') } it 'returns an empty DiffCollection' do - expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection) - expect(mr_diff.diffs).to be_empty + expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection) + expect(mr_diff.raw_diffs).to be_empty end end context 'when the raw diffs exist' do it 'returns the diffs' do - expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection) - expect(mr_diff.diffs).not_to be_empty + expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection) + expect(mr_diff.raw_diffs).not_to be_empty end context 'when the :paths option is set' do - let(:diffs) { mr_diff.diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) } + let(:diffs) { mr_diff.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) } it 'only returns diffs that match the (old path, new path) given' do expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb') diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e43008c1a4d..d793cfd0bde 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -136,7 +136,7 @@ describe MergeRequest, models: true do it 'delegates to the MR diffs' do merge_request.merge_request_diff = MergeRequestDiff.new - expect(merge_request.merge_request_diff).to receive(:diffs).with(options) + expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options) merge_request.raw_diffs(options) end @@ -161,7 +161,7 @@ describe MergeRequest, models: true do it 'delegates to the MR diffs' do merge_request.merge_request_diff = MergeRequestDiff.new - expect(merge_request.merge_request_diff).to receive(:diffs).with(hash_including(options)) + expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)) merge_request.diffs(options) end