summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/build/rules/rule/clause/exists.rb
blob: c55615bb83bdb6a46f678ce2e08811419eb1edf6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# frozen_string_literal: true

module Gitlab
  module Ci
    module Build
      class Rules::Rule::Clause::Exists < Rules::Rule::Clause
        # The maximum number of patterned glob comparisons that will be
        # performed before the rule assumes that it has a match
        MAX_PATTERN_COMPARISONS = 10_000

        def initialize(globs)
          @globs = Array(globs)
          @top_level_only = @globs.all?(&method(:top_level_glob?))
        end

        def satisfied_by?(_pipeline, context)
          paths = worktree_paths(context)
          exact_globs, pattern_globs = separate_globs(context)

          exact_matches?(paths, exact_globs) || pattern_matches?(paths, pattern_globs)
        end

        private

        def separate_globs(context)
          expanded_globs = expand_globs(context)
          expanded_globs.partition(&method(:exact_glob?))
        end

        def expand_globs(context)
          @globs.map do |glob|
            ExpandVariables.expand_existing(glob, -> { context.variables_hash })
          end
        end

        def worktree_paths(context)
          return [] unless context.project

          if @top_level_only
            context.top_level_worktree_paths
          else
            context.all_worktree_paths
          end
        end

        def exact_matches?(paths, exact_globs)
          exact_globs.any? do |glob|
            paths.bsearch { |path| glob <=> path }
          end
        end

        def pattern_matches?(paths, pattern_globs)
          comparisons = 0

          pattern_globs.any? do |glob|
            paths.any? do |path|
              comparisons += 1
              comparisons > MAX_PATTERN_COMPARISONS || pattern_match?(glob, path)
            end
          end
        end

        def pattern_match?(glob, path)
          File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
        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 exact_glob?(glob)
          !glob.include?('*') && !glob.include?('?') && !glob.include?('[') && !glob.include?('{')
        end
      end
    end
  end
end