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

operation_service.rb « git « gitlab « lib « ruby - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 0ae429be080b10b91eedb5075e84906c5a4b386d (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
module Gitlab
  module Git
    class OperationService
      include Gitlab::Git::Popen

      BranchUpdate = Struct.new(:newrev, :repo_created, :branch_created) do
        alias_method :repo_created?, :repo_created
        alias_method :branch_created?, :branch_created

        def self.from_gitaly(branch_update)
          return if branch_update.nil?

          new(
            branch_update.commit_id,
            branch_update.repo_created,
            branch_update.branch_created
          )
        end
      end

      attr_reader :user, :repository

      def initialize(user, new_repository)
        @user = user
        @repository = new_repository
      end

      # Whenever `start_branch_name` or `start_sha` is passed, if `branch_name`
      # doesn't exist, it will be created from the commit pointed to by
      # `start_branch_name` or `start_sha`.
      #
      # If `start_repository` is passed, and the branch doesn't exist,
      # it would try to find the commits from it instead of current repository.
      def with_branch(branch_name,
                      start_branch_name: nil,
                      start_sha: nil,
                      start_repository: repository,
                      force: false,
                      &block)
        start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)

        start_branch_name = nil if start_repository.empty?

        if start_branch_name.present? && !start_repository.branch_exists?(start_branch_name)
          raise ArgumentError, "Cannot find branch '#{start_branch_name}'"
        elsif start_sha.present? && !start_repository.commit_id(start_sha)
          raise ArgumentError, "Cannot find commit '#{start_sha}'"
        end

        update_branch_with_hooks(branch_name, force) do
          repository.with_repo_branch_commit(
            start_repository,
            start_sha.presence || start_branch_name.presence || branch_name,
            &block
          )
        end
      end

      def update_branch(branch_name, newrev, oldrev, push_options: nil, transaction: nil)
        ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
        update_ref_in_hooks(ref, newrev, oldrev, push_options: push_options, transaction: transaction)
      end

      # Yields the given block (which should return a commit) and
      # writes it to the ref while also executing hooks for it.
      # The ref is _always_ overwritten (nothing is taken from its
      # previous state).
      #
      # Returns the generated commit.
      #
      # ref - The target ref path we're committing to.
      # from_ref - The ref we're taking the HEAD commit from.
      def commit_ref(ref, source_sha, from_ref:)
        update_autocrlf_option

        target_sha = from_ref.target
        repository.write_ref(ref, target_sha)

        # Make commit
        newrev = yield

        unless newrev
          error = "Failed to create merge commit for source_sha #{source_sha} and" \
                  " target_sha #{target_sha} at #{ref}"

          raise Gitlab::Git::CommitError.new(error)
        end

        oldrev = from_ref.target

        update_ref(ref, newrev, oldrev)

        newrev
      end

      private

      # Returns [newrev, should_run_after_create, should_run_after_create_branch]
      def update_branch_with_hooks(branch_name, force)
        update_autocrlf_option

        was_empty = repository.empty?

        # Make commit
        newrev = yield

        raise Gitlab::Git::CommitError.new('Failed to create commit') unless newrev

        branch = repository.find_branch(branch_name)
        oldrev = find_oldrev_from_branch(newrev, branch, force)

        ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
        update_ref_in_hooks(ref, newrev, oldrev)

        BranchUpdate.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev))
      end

      def find_oldrev_from_branch(newrev, branch, force)
        return Gitlab::Git::BLANK_SHA unless branch

        oldrev = branch.target

        return oldrev if force

        merge_base = repository.merge_base(newrev, branch.target)
        raise Gitlab::Git::Repository::InvalidRef unless merge_base

        if oldrev == merge_base
          oldrev
        else
          raise Gitlab::Git::CommitError.new('Branch diverged')
        end
      end

      def update_ref_in_hooks(ref, newrev, oldrev, push_options: nil, transaction: nil)
        with_hooks(ref, newrev, oldrev, push_options: push_options, transaction: transaction) do
          update_ref(ref, newrev, oldrev)
        end
      end

      def with_hooks(ref, newrev, oldrev, push_options: nil, transaction: nil)
        Gitlab::Git::HooksService.new.execute(
          user,
          repository,
          oldrev,
          newrev,
          ref,
          push_options: push_options,
          transaction: transaction
        ) do |service|
          yield(service)
        end
      end

      def update_ref(ref, newrev, oldrev)
        # We use 'git update-ref' because libgit2/rugged currently does not
        # offer 'compare and swap' ref updates. Without compare-and-swap we can
        # (and have!) accidentally reset the ref to an earlier state, clobbering
        # commits. See also https://github.com/libgit2/libgit2/issues/1534.
        command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]

        output, status = popen(
          command,
          repository.path
        ) do |stdin|
          stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
        end

        unless status.zero?
          Gitlab::GitLogger.error("'git update-ref' in #{repository.path}: #{output}")
          ref_name = Gitlab::Git.branch_name(ref) || ref

          raise Gitlab::Git::CommitError.new(
            "Could not update #{ref_name}." \
            " Please refresh and try again."
          )
        end
      end

      def update_autocrlf_option
        repository.autocrlf = :input if repository.autocrlf != :input
      end
    end
  end
end