summaryrefslogtreecommitdiff
path: root/lib/gitlab/git/operation_service.rb
blob: 280def182d5ceb5b08dcf8b977b5ff91100dfa18 (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
186
187
188
189
190
191
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)
          new(
            branch_update.commit_id,
            branch_update.repo_created,
            branch_update.branch_created
          )
        end
      end

      attr_reader :user, :repository

      def initialize(user, new_repository)
        if user
          user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
          @user = user
        end

        # Refactoring aid
        Gitlab::Git.check_namespace!(new_repository)

        @repository = new_repository
      end

      def add_branch(branch_name, newrev)
        ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
        oldrev = Gitlab::Git::BLANK_SHA

        update_ref_in_hooks(ref, newrev, oldrev)
      end

      def rm_branch(branch)
        ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
        oldrev = branch.target
        newrev = Gitlab::Git::BLANK_SHA

        update_ref_in_hooks(ref, newrev, oldrev)
      end

      def add_tag(tag_name, newrev, options = {})
        ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
        oldrev = Gitlab::Git::BLANK_SHA

        with_hooks(ref, newrev, oldrev) do |service|
          # We want to pass the OID of the tag object to the hooks. For an
          # annotated tag we don't know that OID until after the tag object
          # (raw_tag) is created in the repository. That is why we have to
          # update the value after creating the tag object. Only the
          # "post-receive" hook will receive the correct value in this case.
          raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
          service.newrev = raw_tag.target_id
        end
      end

      def rm_tag(tag)
        ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
        oldrev = tag.target
        newrev = Gitlab::Git::BLANK_SHA

        update_ref_in_hooks(ref, newrev, oldrev) do
          repository.rugged.tags.delete(tag_name)
        end
      end

      # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
      # it would be created from `start_branch_name`.
      # 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_repository: repository,
        &block)

        Gitlab::Git.check_namespace!(start_repository)
        start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)

        start_branch_name = nil if start_repository.empty?

        if start_branch_name && !start_repository.branch_exists?(start_branch_name)
          raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.relative_path}"
        end

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

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

      private

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

        was_empty = repository.empty?

        # Make commit
        newrev = yield

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

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

        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)
        return Gitlab::Git::BLANK_SHA unless branch

        oldrev = branch.target

        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)
        with_hooks(ref, newrev, oldrev) do
          update_ref(ref, newrev, oldrev)
        end
      end

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

          yield(service)
        end
      end

      # Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites.
      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}")
          raise Gitlab::Git::CommitError.new(
            "Could not update branch #{Gitlab::Git.branch_name(ref)}." \
            " Please refresh and try again.")
        end
      end

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