summaryrefslogtreecommitdiff
path: root/lib/gitlab/checks/changes_access.rb
blob: 3676189fe3750c8eb1e23229d8f1d747f7c12ecd (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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# frozen_string_literal: true

module Gitlab
  module Checks
    class ChangesAccess
      include Gitlab::Utils::StrongMemoize

      ATTRIBUTES = %i[user_access project protocol changes logger].freeze

      attr_reader(*ATTRIBUTES)

      def initialize(
        changes, user_access:, project:, protocol:, logger:
      )
        @changes = changes
        @user_access = user_access
        @project = project
        @protocol = protocol
        @logger = logger
      end

      def validate!
        return if changes.empty?

        single_access_checks!

        logger.log_timed("Running checks for #{changes.length} changes") do
          bulk_access_checks!
        end

        true
      end

      # All commits which have been newly introduced via any of the given
      # changes. This set may also contain commits which are not referenced by
      # any of the new revisions.
      def commits
        strong_memoize(:commits) do
          newrevs = @changes.filter_map do |change|
            newrev = change[:newrev]

            next if blank_rev?(newrev)

            newrev
          end

          next [] if newrevs.empty?

          project.repository.new_commits(newrevs)
        end
      end

      # All commits which have been newly introduced via the given revision.
      def commits_for(oldrev, newrev)
        commits_by_id = commits.index_by(&:id)

        result = []
        pending = Set[newrev]

        # We go up the parent chain of our newrev and collect all commits which
        # are new. In case a commit's ID cannot be found in the set of new
        # commits, then it must already be a preexisting commit.
        while pending.any?
          rev = pending.first
          pending.delete(rev)

          # Remove the revision from commit candidates such that we don't walk
          # it multiple times. If the hash doesn't contain the revision, then
          # we have either already walked the commit or it's not new.
          commit = commits_by_id.delete(rev)
          next if commit.nil?

          # Only add the parent ID to the pending set if we actually know its
          # commit to guards us against readding an ID which we have already
          # queued up before. Furthermore, we stop walking as soon as we hit
          # `oldrev` such that we do not include any commits in our checks
          # which have been "over-pushed" by the client.
          commit.parent_ids.each do |parent_id|
            pending.add(parent_id) if commits_by_id.has_key?(parent_id) && parent_id != oldrev
          end

          result << commit
        end

        result
      end

      def single_change_accesses
        @single_changes_accesses ||=
          changes.map do |change|
            commits =
              if blank_rev?(change[:newrev])
                []
              else
                Gitlab::Lazy.new { commits_for(change[:oldrev], change[:newrev]) }
              end

            Checks::SingleChangeAccess.new(
              change,
              user_access: user_access,
              project: project,
              protocol: protocol,
              logger: logger,
              commits: commits
            )
          end
      end

      protected

      def single_access_checks!
        # Iterate over all changes to find if user allowed all of them to be applied
        single_change_accesses.each do |single_change_access|
          single_change_access.validate!
        end
      end

      def bulk_access_checks!
        Gitlab::Checks::LfsCheck.new(self).validate!
      end

      def blank_rev?(rev)
        rev.blank? || Gitlab::Git.blank_ref?(rev)
      end
    end
  end
end

Gitlab::Checks::ChangesAccess.prepend_mod_with('Gitlab::Checks::ChangesAccess')