operation_service.rb 5.46 KB
Newer Older
1 2 3
module Gitlab
  module Git
    class OperationService
4 5
      include Gitlab::Git::Popen

6 7 8 9 10
      WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do
        alias_method :repo_created?, :repo_created
        alias_method :branch_created?, :branch_created
      end

11
      attr_reader :user, :repository
12

13 14 15 16 17
      def initialize(user, new_repository)
        if user
          user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
          @user = user
        end
18 19

        # Refactoring aid
20
        Gitlab::Git.check_namespace!(new_repository)
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

        @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_project` 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)

        # Refactoring aid
        unless start_repository.is_a?(Gitlab::Git::Repository)
          raise "expected a Gitlab::Git::Repository, got #{start_repository}"
        end

        start_branch_name = nil if start_repository.empty_repo?

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

        update_branch_with_hooks(branch_name) do
          repository.with_repo_branch_commit(
            start_repository,
            start_branch_name || branch_name,
            &block)
        end
      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)

115
        WithBranchResult.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev))
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
      end

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

        oldrev = branch.target

        if oldrev == repository.rugged.merge_base(newrev, branch.target)
          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(
138
          user,
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
          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]
155
        _, status = popen(
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
          command,
          repository.path) do |stdin|
          stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
        end

        unless status.zero?
          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