Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Fargher <proglottis@gmail.com>2019-09-11 04:53:24 +0300
committerJames Fargher <proglottis@gmail.com>2019-09-11 06:19:48 +0300
commit20f6f34f162584942dd3a31cd8168549078f5652 (patch)
tree6a921f740c338e24524d6345bb1ecc7a36774628
parent895b6064f3568c306dda3180395b6589d1187828 (diff)
Add file matching rule to flexible CI rulesjob_file_matching
This allows support for rules based on files in the repository: ``` job: script: - echo Dockerfile exists rules: - local: - Dockerfile ```
-rw-r--r--changelogs/unreleased/job_file_matching.yml5
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/local.rb54
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb8
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb32
5 files changed, 125 insertions, 4 deletions
diff --git a/changelogs/unreleased/job_file_matching.yml b/changelogs/unreleased/job_file_matching.yml
new file mode 100644
index 00000000000..99723f76199
--- /dev/null
+++ b/changelogs/unreleased/job_file_matching.yml
@@ -0,0 +1,5 @@
+---
+title: Add file matching rule to flexible CI rules
+merge_request: 32911
+author:
+type: added
diff --git a/lib/gitlab/ci/build/rules/rule/clause/local.rb b/lib/gitlab/ci/build/rules/rule/clause/local.rb
new file mode 100644
index 00000000000..8865d1f2b33
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/local.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::Local < Rules::Rule::Clause
+ MATCH_LIMIT = 10_000
+
+ def initialize(globs)
+ @globs = Array(globs)
+ end
+
+ def satisfied_by?(pipeline, seed)
+ paths = if top_level_only?
+ pipeline.project.repository.tree(pipeline.sha).blobs.map(&:path)
+ else
+ pipeline.project.repository.ls_files(pipeline.sha)
+ end
+
+ simple, complex = @globs.partition { |glob| simple_glob?(glob) }
+
+ matches = 0
+ simple.any? { |glob| paths.bsearch { |path| glob <=> path } } ||
+ complex.any? do |glob|
+ paths.any? do |path|
+ matches += 1
+ matches > MATCH_LIMIT || glob_match?(glob, path)
+ end
+ end
+ end
+
+ private
+
+ def glob_match?(glob, path)
+ File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
+ end
+
+ def top_level_only?
+ @globs.all? { |glob| top_level_glob?(glob) }
+ end
+
+ # matches glob patterns that only match files in the top level directory
+ def top_level_glob?(glob)
+ !glob.include?('/') && !glob.include?('**')
+ end
+
+ # matches glob patterns that have no metacharacters for File#fnmatch?
+ def simple_glob?(glob)
+ !glob.include?('*') && !glob.include?('?') && !glob.include?('[') && !glob.include?('{')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
index 1f2a34ec90e..3d5d0c7ae07 100644
--- a/lib/gitlab/ci/config/entry/rules/rule.rb
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -8,11 +8,11 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- CLAUSES = %i[if changes].freeze
- ALLOWED_KEYS = %i[if changes when start_in].freeze
+ CLAUSES = %i[if changes local].freeze
+ ALLOWED_KEYS = %i[if changes local when start_in].freeze
ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
- attributes :if, :changes, :when, :start_in
+ attributes :if, :changes, :local, :when, :start_in
validations do
validates :config, presence: true
@@ -24,7 +24,7 @@ module Gitlab
with_options allow_nil: true do
validates :if, expression: true
- validates :changes, array_of_strings: true
+ validates :changes, :local, array_of_strings: true
validates :when, allowed_values: { in: ALLOWED_WHEN }
end
end
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb
new file mode 100644
index 00000000000..0b82373880a
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Rules::Rule::Clause::Local do
+ describe 'satisfied_by?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:case_name, :globs, :files, :satisfied) do
+ 'simple top-level match' | ['Dockerfile'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true
+ 'simple top-level no match' | ['Dockerfile'] | { 'Gemfile' => '' } | false
+ 'complex top-level match' | ['Docker*'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true
+ 'complex top-level no match' | ['Docker*'] | { 'Gemfile' => '' } | false
+ 'simple nested match' | ['project/build.properties'] | { 'project/build.properties' => '' } | true
+ 'simple nested no match' | ['project/build.properties'] | { 'project/README.md' => '' } | false
+ 'complex nested match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/goproject.go' => '' } | true
+ 'complex nested no match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/README.md' => '' } | false
+ end
+
+ with_them do
+ let(:project) { create(:project, :custom_repo, files: files) }
+ let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
+ subject { described_class.new(globs) }
+
+ it 'checks if any files exist' do
+ expect(subject.satisfied_by?(pipeline, nil)).to eq(satisfied)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index c25344ec1a4..ba161d9c7b3 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -103,6 +103,32 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
end
end
+ context 'when using a local: clause' do
+ let(:config) { { local: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when using a string as an invalid local: clause' do
+ let(:config) { { local: 'a regular string' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid policy' do
+ expect(subject.errors).to include(/should be an array of strings/)
+ end
+ end
+
+ context 'when using a list as an invalid local: clause' do
+ let(:config) { { local: [1, 2] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns errors' do
+ expect(subject.errors).to include(/local should be an array of strings/)
+ end
+ end
+
context 'specifying a delayed job' do
let(:config) { { if: '$THIS || $THAT', when: 'delayed', start_in: '15 minutes' } }
@@ -198,6 +224,12 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
expect(entry.value).to eq(config)
end
end
+
+ context 'when using a local: clause' do
+ let(:config) { { local: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to eq(config) }
+ end
end
describe '.default' do