repository.rb 32.2 KB
Newer Older
1 2
# frozen_string_literal: true

Robert Speicher's avatar
Robert Speicher committed
3 4 5 6 7 8 9
require 'tempfile'
require 'forwardable'
require "rubygems/package"

module Gitlab
  module Git
    class Repository
10
      include Gitlab::Git::RepositoryMirroring
11
      include Gitlab::Git::WrapsGitalyErrors
12
      include Gitlab::EncodingHelper
13
      include Gitlab::Utils::StrongMemoize
Robert Speicher's avatar
Robert Speicher committed
14 15

      SEARCH_CONTEXT_LINES = 3
16
      REV_LIST_COMMIT_LIMIT = 2_000
17 18 19
      # In https://gitlab.com/gitlab-org/gitaly/merge_requests/698
      # We copied these two prefixes into gitaly-go, so don't change these
      # or things will break! (REBASE_WORKTREE_PREFIX and SQUASH_WORKTREE_PREFIX)
20 21
      REBASE_WORKTREE_PREFIX = 'rebase'.freeze
      SQUASH_WORKTREE_PREFIX = 'squash'.freeze
22
      GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
23
      GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
24
      EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze
Robert Speicher's avatar
Robert Speicher committed
25

26
      NoRepository = Class.new(StandardError)
27
      InvalidRepository = Class.new(StandardError)
28 29
      InvalidBlobName = Class.new(StandardError)
      InvalidRef = Class.new(StandardError)
30
      GitError = Class.new(StandardError)
31
      DeleteBranchError = Class.new(StandardError)
32
      CreateTreeError = Class.new(StandardError)
33
      TagExistsError = Class.new(StandardError)
34
      ChecksumError = Class.new(StandardError)
Robert Speicher's avatar
Robert Speicher committed
35

36
      class << self
37 38 39 40 41 42 43 44 45 46 47 48
        def create_hooks(repo_path, global_hooks_path)
          local_hooks_path = File.join(repo_path, 'hooks')
          real_local_hooks_path = :not_found

          begin
            real_local_hooks_path = File.realpath(local_hooks_path)
          rescue Errno::ENOENT
            # real_local_hooks_path == :not_found
          end

          # Do nothing if hooks already exist
          unless real_local_hooks_path == File.realpath(global_hooks_path)
Rémy Coutable's avatar
Rémy Coutable committed
49 50 51 52 53 54
            if File.exist?(local_hooks_path)
              # Move the existing hooks somewhere safe
              FileUtils.mv(
                local_hooks_path,
                "#{local_hooks_path}.old.#{Time.now.to_i}")
            end
55 56 57

            # Create the hooks symlink
            FileUtils.ln_sf(global_hooks_path, local_hooks_path)
58 59 60 61 62 63
          end

          true
        end
      end

Robert Speicher's avatar
Robert Speicher committed
64 65 66
      # Directory name of repo
      attr_reader :name

67 68 69
      # Relative path of repo
      attr_reader :relative_path

70
      attr_reader :storage, :gl_repository, :relative_path, :gl_project_path
Jacob Vosmaer's avatar
Jacob Vosmaer committed
71

72 73 74 75 76 77 78
      # This remote name has to be stable for all types of repositories that
      # can join an object pool. If it's structure ever changes, a migration
      # has to be performed on the object pools to update the remote names.
      # Else the pool can't be updated anymore and is left in an inconsistent
      # state.
      alias_method :object_pool_remote_name, :gl_repository

79 80
      # This initializer method is only used on the client side (gitlab-ce).
      # Gitaly-ruby uses a different initializer.
81
      def initialize(storage, relative_path, gl_repository, gl_project_path)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
82
        @storage = storage
83
        @relative_path = relative_path
84
        @gl_repository = gl_repository
85
        @gl_project_path = gl_project_path
86 87

        @name = @relative_path.split("/").last
Robert Speicher's avatar
Robert Speicher committed
88 89
      end

90
      def ==(other)
91 92 93 94 95 96 97
        other.is_a?(self.class) && [storage, relative_path] == [other.storage, other.relative_path]
      end

      alias_method :eql?, :==

      def hash
        [self.class, storage, relative_path].hash
98 99
      end

100
      # This method will be removed when Gitaly reaches v1.1.
101
      def path
102
        File.join(
103 104 105 106
          Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path
        )
      end

Robert Speicher's avatar
Robert Speicher committed
107 108
      # Default branch in the repository
      def root_ref
109 110 111 112 113
        gitaly_ref_client.default_branch_name
      rescue GRPC::NotFound => e
        raise NoRepository.new(e.message)
      rescue GRPC::Unknown => e
        raise Gitlab::Git::CommandError.new(e.message)
Robert Speicher's avatar
Robert Speicher committed
114 115
      end

116
      def exists?
117
        gitaly_repository_client.exists?
118 119
      end

Robert Speicher's avatar
Robert Speicher committed
120 121 122
      # Returns an Array of branch names
      # sorted by name ASC
      def branch_names
123 124
        wrapped_gitaly_errors do
          gitaly_ref_client.branch_names
125
        end
Robert Speicher's avatar
Robert Speicher committed
126 127 128
      end

      # Returns an Array of Branches
129
      def branches
130 131
        wrapped_gitaly_errors do
          gitaly_ref_client.branches
132
        end
Robert Speicher's avatar
Robert Speicher committed
133 134 135 136
      end

      # Directly find a branch with a simple name (e.g. master)
      #
137 138 139
      def find_branch(name)
        wrapped_gitaly_errors do
          gitaly_ref_client.find_branch(name)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
140
        end
Robert Speicher's avatar
Robert Speicher committed
141 142
      end

143
      def local_branches(sort_by: nil)
144 145
        wrapped_gitaly_errors do
          gitaly_ref_client.local_branches(sort_by: sort_by)
Robert Speicher's avatar
Robert Speicher committed
146 147 148 149 150
        end
      end

      # Returns the number of valid branches
      def branch_count
151 152
        wrapped_gitaly_errors do
          gitaly_ref_client.count_branch_names
153 154
        end
      end
Robert Speicher's avatar
Robert Speicher committed
155

156 157 158 159
      def expire_has_local_branches_cache
        clear_memoization(:has_local_branches)
      end

160
      def has_local_branches?
161 162 163 164 165
        strong_memoize(:has_local_branches) do
          uncached_has_local_branches?
        end
      end

166 167 168 169 170 171 172
      # Git repository can contains some hidden refs like:
      #   /refs/notes/*
      #   /refs/git-as-svn/*
      #   /refs/pulls/*
      # This refs by default not visible in project page and not cloned to client side.
      alias_method :has_visible_content?, :has_local_branches?

173 174
      # Returns the number of valid tags
      def tag_count
175 176
        wrapped_gitaly_errors do
          gitaly_ref_client.count_tag_names
Robert Speicher's avatar
Robert Speicher committed
177 178 179 180 181
        end
      end

      # Returns an Array of tag names
      def tag_names
182 183 184
        wrapped_gitaly_errors do
          gitaly_ref_client.tag_names
        end
Robert Speicher's avatar
Robert Speicher committed
185 186 187
      end

      # Returns an Array of Tags
Jacob Vosmaer's avatar
Jacob Vosmaer committed
188
      #
Robert Speicher's avatar
Robert Speicher committed
189
      def tags
190 191
        wrapped_gitaly_errors do
          gitaly_ref_client.tags
192
        end
Robert Speicher's avatar
Robert Speicher committed
193 194
      end

195 196 197 198
      # Returns true if the given ref name exists
      #
      # Ref names must start with `refs/`.
      def ref_exists?(ref_name)
199 200
        wrapped_gitaly_errors do
          gitaly_ref_exists?(ref_name)
201 202 203
        end
      end

Robert Speicher's avatar
Robert Speicher committed
204 205 206 207
      # Returns true if the given tag exists
      #
      # name - The name of the tag as a String.
      def tag_exists?(name)
208 209
        wrapped_gitaly_errors do
          gitaly_ref_exists?("refs/tags/#{name}")
210
        end
Robert Speicher's avatar
Robert Speicher committed
211 212 213 214 215 216
      end

      # Returns true if the given branch exists
      #
      # name - The name of the branch as a String.
      def branch_exists?(name)
217 218
        wrapped_gitaly_errors do
          gitaly_ref_exists?("refs/heads/#{name}")
219
        end
Robert Speicher's avatar
Robert Speicher committed
220 221 222 223 224 225 226
      end

      # Returns an Array of branch and tag names
      def ref_names
        branch_names + tag_names
      end

227
      def delete_all_refs_except(prefixes)
228 229
        wrapped_gitaly_errors do
          gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
230
        end
231 232
      end

233
      def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:)
Robert Speicher's avatar
Robert Speicher committed
234 235 236 237
        ref ||= root_ref
        commit = Gitlab::Git::Commit.find(self, ref)
        return {} if commit.nil?

238
        prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha)
Robert Speicher's avatar
Robert Speicher committed
239 240 241

        {
          'ArchivePrefix' => prefix,
242
          'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format),
243 244
          'CommitId' => commit.id,
          'GitalyRepository' => gitaly_repository.to_h
Robert Speicher's avatar
Robert Speicher committed
245 246 247
        }
      end

248 249
      # This is both the filename of the archive (missing the extension) and the
      # name of the top-level member of the archive under which all files go
250
      def archive_prefix(ref, sha, project_path, append_sha:)
251 252 253 254
        append_sha = (ref != sha) if append_sha.nil?

        formatted_ref = ref.tr('/', '-')

255
        prefix_segments = [project_path, formatted_ref]
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
        prefix_segments << sha if append_sha

        prefix_segments.join('-')
      end
      private :archive_prefix

      # The full path on disk where the archive should be stored. This is used
      # to cache the archive between requests.
      #
      # The path is a global namespace, so needs to be globally unique. This is
      # achieved by including `gl_repository` in the path.
      #
      # Archives relating to a particular ref when the SHA is not present in the
      # filename must be invalidated when the ref is updated to point to a new
      # SHA. This is achieved by including the SHA in the path.
      #
      # As this is a full path on disk, it is not "cloud native". This should
      # be resolved by either removing the cache, or moving the implementation
      # into Gitaly and removing the ArchivePath parameter from the git-archive
      # senddata response.
      def archive_file_path(storage_path, sha, name, format = "tar.gz")
Robert Speicher's avatar
Robert Speicher committed
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
        # Build file path
        return nil unless name

        extension =
          case format
          when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
            "tar.bz2"
          when "tar"
            "tar"
          when "zip"
            "zip"
          else
            # everything else should fall back to tar.gz
            "tar.gz"
          end

        file_name = "#{name}.#{extension}"
294
        File.join(storage_path, self.gl_repository, sha, file_name)
Robert Speicher's avatar
Robert Speicher committed
295
      end
296
      private :archive_file_path
Robert Speicher's avatar
Robert Speicher committed
297 298 299

      # Return repo size in megabytes
      def size
300
        size = gitaly_repository_client.repository_size
301

Robert Speicher's avatar
Robert Speicher committed
302 303 304
        (size.to_f / 1024).round(2)
      end

305
      # Build an array of commits.
Robert Speicher's avatar
Robert Speicher committed
306 307 308 309 310 311 312 313 314 315
      #
      # Usage.
      #   repo.log(
      #     ref: 'master',
      #     path: 'app/models',
      #     limit: 10,
      #     offset: 5,
      #     after: Time.new(2016, 4, 21, 14, 32, 10)
      #   )
      def log(options)
316 317 318 319 320 321 322
        default_options = {
          limit: 10,
          offset: 0,
          path: nil,
          follow: false,
          skip_merges: false,
          after: nil,
Tiago Botelho's avatar
Tiago Botelho committed
323 324
          before: nil,
          all: false
325 326 327 328 329
        }

        options = default_options.merge(options)
        options[:offset] ||= 0

330 331 332 333 334
        limit = options[:limit]
        if limit == 0 || !limit.is_a?(Integer)
          raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
        end

335 336
        wrapped_gitaly_errors do
          gitaly_commit_client.find_commits(options)
337
        end
Robert Speicher's avatar
Robert Speicher committed
338 339
      end

340
      def new_commits(newrev)
341 342
        wrapped_gitaly_errors do
          gitaly_ref_client.list_new_commits(newrev)
343 344 345
        end
      end

346
      def new_blobs(newrev)
347
        return [] if newrev.blank? || newrev == ::Gitlab::Git::BLANK_SHA
348 349 350 351 352 353 354 355

        strong_memoize("new_blobs_#{newrev}") do
          wrapped_gitaly_errors do
            gitaly_ref_client.list_new_blobs(newrev, REV_LIST_COMMIT_LIMIT)
          end
        end
      end

356
      def count_commits(options)
357
        options = process_count_commits_options(options.dup)
358

359 360 361 362 363 364 365 366 367 368 369
        wrapped_gitaly_errors do
          if options[:left_right]
            from = options[:from]
            to = options[:to]

            right_count = gitaly_commit_client
              .commit_count("#{from}..#{to}", options)
            left_count = gitaly_commit_client
              .commit_count("#{to}..#{from}", options)

            [left_count, right_count]
370
          else
371
            gitaly_commit_client.commit_count(options[:ref], options)
372 373
          end
        end
374 375
      end

Robert Speicher's avatar
Robert Speicher committed
376
      # Counts the amount of commits between `from` and `to`.
377 378
      def count_commits_between(from, to, options = {})
        count_commits(from: from, to: to, **options)
Robert Speicher's avatar
Robert Speicher committed
379 380
      end

Rubén Dávila's avatar
Rubén Dávila committed
381 382 383
      # old_rev and new_rev are commit ID's
      # the result of this method is an array of Gitlab::Git::RawDiffChange
      def raw_changes_between(old_rev, new_rev)
384 385
        @raw_changes_between ||= {}

386 387 388
        @raw_changes_between[[old_rev, new_rev]] ||=
          begin
            return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
389

390
            wrapped_gitaly_errors do
391 392 393 394 395
              gitaly_repository_client.raw_changes_between(old_rev, new_rev)
                .each_with_object([]) do |msg, arr|
                msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) }
              end
            end
Rubén Dávila's avatar
Rubén Dávila committed
396
          end
397 398
      rescue ArgumentError => e
        raise Gitlab::Git::Repository::GitError.new(e)
Rubén Dávila's avatar
Rubén Dávila committed
399 400
      end

Robert Speicher's avatar
Robert Speicher committed
401
      # Returns the SHA of the most recent common ancestor of +from+ and +to+
402
      def merge_base(*commits)
403
        wrapped_gitaly_errors do
404
          gitaly_repository_client.find_merge_base(*commits)
405
        end
Robert Speicher's avatar
Robert Speicher committed
406 407
      end

408
      # Returns true is +from+ is direct ancestor to +to+, otherwise false
409
      def ancestor?(from, to)
410
        gitaly_commit_client.ancestor?(from, to)
411 412
      end

413
      def merged_branch_names(branch_names = [])
414 415 416 417 418 419
        return [] unless root_ref

        root_sha = find_branch(root_ref)&.target

        return [] unless root_sha

420 421
        branches = wrapped_gitaly_errors do
          gitaly_merged_branch_names(branch_names, root_sha)
422 423 424
        end

        Set.new(branches)
425 426
      end

Robert Speicher's avatar
Robert Speicher committed
427 428 429 430 431
      # Return an array of Diff objects that represent the diff
      # between +from+ and +to+.  See Diff::filter_diff_options for the allowed
      # diff options.  The +options+ hash can also include :break_rewrites to
      # split larger rewrites into delete/add pairs.
      def diff(from, to, options = {}, *paths)
432
        iterator = gitaly_commit_client.diff(from, to, options.merge(paths: paths))
433 434

        Gitlab::Git::DiffCollection.new(iterator, options)
Robert Speicher's avatar
Robert Speicher committed
435 436
      end

437
      def diff_stats(left_id, right_id)
438 439 440 441
        if [left_id, right_id].any? { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) }
          return empty_diff_stats
        end

442 443 444 445 446
        stats = wrapped_gitaly_errors do
          gitaly_commit_client.diff_stats(left_id, right_id)
        end

        Gitlab::Git::DiffStatsCollection.new(stats)
447
      rescue CommandError, TypeError
448
        empty_diff_stats
449
      end
Robert Speicher's avatar
Robert Speicher committed
450

451 452
      # Returns a RefName for a given SHA
      def ref_name_for_sha(ref_path, sha)
453 454
        raise ArgumentError, "sha can't be empty" unless sha.present?

455
        gitaly_ref_client.find_ref_name(sha, ref_path)
456 457
      end

Takuya Noguchi's avatar
Takuya Noguchi committed
458
      # Get refs hash which key is the commit id
459 460
      # and value is a Gitlab::Git::Tag or Gitlab::Git::Branch
      # Note that both inherit from Gitlab::Git::Ref
Robert Speicher's avatar
Robert Speicher committed
461
      def refs_hash
462 463 464 465 466 467 468 469
        return @refs_hash if @refs_hash

        @refs_hash = Hash.new { |h, k| h[k] = [] }

        (tags + branches).each do |ref|
          next unless ref.target && ref.name

          @refs_hash[ref.dereferenced_target.id] << ref.name
Robert Speicher's avatar
Robert Speicher committed
470
        end
471

Robert Speicher's avatar
Robert Speicher committed
472 473 474
        @refs_hash
      end

475
      # Returns url for submodule
Robert Speicher's avatar
Robert Speicher committed
476 477
      #
      # Ex.
478 479
      #   @repository.submodule_url_for('master', 'rack')
      #   # => git@localhost:rack.git
Robert Speicher's avatar
Robert Speicher committed
480
      #
481
      def submodule_url_for(ref, path)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
482 483
        wrapped_gitaly_errors do
          gitaly_submodule_url_for(ref, path)
Robert Speicher's avatar
Robert Speicher committed
484 485 486 487 488
        end
      end

      # Return total commits count accessible from passed ref
      def commit_count(ref)
489 490
        wrapped_gitaly_errors do
          gitaly_commit_client.commit_count(ref)
491
        end
Robert Speicher's avatar
Robert Speicher committed
492 493
      end

494 495 496 497 498 499 500
      # Return total diverging commits count
      def diverging_commit_count(from, to, max_count:)
        wrapped_gitaly_errors do
          gitaly_commit_client.diverging_commit_count(from, to, max_count: max_count)
        end
      end

Robert Speicher's avatar
Robert Speicher committed
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
      # Mimic the `git clean` command and recursively delete untracked files.
      # Valid keys that can be passed in the +options+ hash are:
      #
      # :d - Remove untracked directories
      # :f - Remove untracked directories that are managed by a different
      #      repository
      # :x - Remove ignored files
      #
      # The value in +options+ must evaluate to true for an option to take
      # effect.
      #
      # Examples:
      #
      #   repo.clean(d: true, f: true) # Enable the -d and -f options
      #
      #   repo.clean(d: false, x: true) # -x is enabled, -d is not
      def clean(options = {})
        strategies = [:remove_untracked]
        strategies.push(:force) if options[:f]
        strategies.push(:remove_ignored) if options[:x]

        # TODO: implement this method
      end

525
      def add_branch(branch_name, user:, target:)
526 527 528
        wrapped_gitaly_errors do
          gitaly_operation_client.user_create_branch(branch_name, user, target)
        end
529 530
      end

531
      def add_tag(tag_name, user:, target:, message: nil)
532 533
        wrapped_gitaly_errors do
          gitaly_operation_client.add_tag(tag_name, user, target, message)
534 535 536
        end
      end

537
      def update_branch(branch_name, user:, newrev:, oldrev:)
538 539
        wrapped_gitaly_errors do
          gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
540
        end
541 542
      end

543
      def rm_branch(branch_name, user:)
544 545
        wrapped_gitaly_errors do
          gitaly_operation_client.user_delete_branch(branch_name, user)
546
        end
547 548
      end

549
      def rm_tag(tag_name, user:)
550 551
        wrapped_gitaly_errors do
          gitaly_operation_client.rm_tag(tag_name, user)
552
        end
553 554 555 556 557 558
      end

      def find_tag(name)
        tags.find { |tag| tag.name == name }
      end

559 560 561 562 563 564
      def merge_to_ref(user, source_sha, branch, target_ref, message)
        wrapped_gitaly_errors do
          gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message)
        end
      end

565
      def merge(user, source_sha, target_branch, message, &block)
566 567
        wrapped_gitaly_errors do
          gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block)
568 569 570
        end
      end

571
      def ff_merge(user, source_sha, target_branch)
572 573
        wrapped_gitaly_errors do
          gitaly_operation_client.user_ff_branch(user, source_sha, target_branch)
574 575 576
        end
      end

577
      def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
578 579 580 581 582 583 584 585
        args = {
          user: user,
          commit: commit,
          branch_name: branch_name,
          message: message,
          start_branch_name: start_branch_name,
          start_repository: start_repository
        }
586

587 588
        wrapped_gitaly_errors do
          gitaly_operation_client.user_revert(args)
589 590 591 592
        end
      end

      def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
593 594 595 596 597 598 599 600
        args = {
          user: user,
          commit: commit,
          branch_name: branch_name,
          message: message,
          start_branch_name: start_branch_name,
          start_repository: start_repository
        }
601

602 603
        wrapped_gitaly_errors do
          gitaly_operation_client.user_cherry_pick(args)
604 605 606
        end
      end

607 608 609 610 611 612 613 614 615 616 617 618 619 620
      def update_submodule(user:, submodule:, commit_sha:, message:, branch:)
        args = {
          user: user,
          submodule: submodule,
          commit_sha: commit_sha,
          branch: branch,
          message: message
        }

        wrapped_gitaly_errors do
          gitaly_operation_client.user_update_submodule(args)
        end
      end

Robert Speicher's avatar
Robert Speicher committed
621 622
      # Delete the specified branch from the repository
      def delete_branch(branch_name)
623 624
        wrapped_gitaly_errors do
          gitaly_ref_client.delete_branch(branch_name)
625
        end
626
      rescue CommandError => e
627
        raise DeleteBranchError, e
Robert Speicher's avatar
Robert Speicher committed
628
      end
Lin Jen-Shin's avatar
Lin Jen-Shin committed
629

630
      def delete_refs(*ref_names)
631 632
        wrapped_gitaly_errors do
          gitaly_delete_refs(*ref_names)
633
        end
Lin Jen-Shin's avatar
Lin Jen-Shin committed
634
      end
Robert Speicher's avatar
Robert Speicher committed
635 636 637 638 639 640 641

      # Create a new branch named **ref+ based on **stat_point+, HEAD by default
      #
      # Examples:
      #   create_branch("feature")
      #   create_branch("other-feature", "master")
      def create_branch(ref, start_point = "HEAD")
642 643
        wrapped_gitaly_errors do
          gitaly_ref_client.create_branch(ref, start_point)
644
        end
Robert Speicher's avatar
Robert Speicher committed
645 646
      end

647 648
      # If `mirror_refmap` is present the remote is set as mirror with that mapping
      def add_remote(remote_name, url, mirror_refmap: nil)
649 650
        wrapped_gitaly_errors do
          gitaly_remote_client.add_remote(remote_name, url, mirror_refmap)
651
        end
Robert Speicher's avatar
Robert Speicher committed
652 653
      end

654
      def remove_remote(remote_name)
655 656
        wrapped_gitaly_errors do
          gitaly_remote_client.remove_remote(remote_name)
657
        end
658 659
      end

660 661 662 663 664 665 666 667
      def find_remote_root_ref(remote_name)
        return unless remote_name.present?

        wrapped_gitaly_errors do
          gitaly_remote_client.find_remote_root_ref(remote_name)
        end
      end

Robert Speicher's avatar
Robert Speicher committed
668 669 670 671 672 673
      # Returns result like "git ls-files" , recursive and full file path
      #
      # Ex.
      #   repo.ls_files('master')
      #
      def ls_files(ref)
674
        gitaly_commit_client.ls_files(ref)
Robert Speicher's avatar
Robert Speicher committed
675 676 677
      end

      def copy_gitattributes(ref)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
678 679
        wrapped_gitaly_errors do
          gitaly_repository_client.apply_gitattributes(ref)
Robert Speicher's avatar
Robert Speicher committed
680 681 682
        end
      end

683 684 685
      def info_attributes
        return @info_attributes if @info_attributes

686
        content = gitaly_repository_client.info_attributes
687 688 689
        @info_attributes = AttributesParser.new(content)
      end

Robert Speicher's avatar
Robert Speicher committed
690 691 692 693
      # Returns the Git attributes for the given file path.
      #
      # See `Gitlab::Git::Attributes` for more information.
      def attributes(path)
694
        info_attributes.attributes(path)
Robert Speicher's avatar
Robert Speicher committed
695 696
      end

Sean McGivern's avatar
Sean McGivern committed
697 698 699 700
      def gitattribute(path, name)
        attributes(path)[name]
      end

701 702 703 704 705
      # Check .gitattributes for a given ref
      #
      # This only checks the root .gitattributes file,
      # it does not traverse subfolders to find additional .gitattributes files
      #
706 707 708
      # This method is around 30 times slower than `attributes`, which uses
      # `$GIT_DIR/info/attributes`. Consider caching AttributesAtRefParser
      # and reusing that for multiple calls instead of this method.
709 710 711 712 713
      def attributes_at(ref, file_path)
        parser = AttributesAtRefParser.new(self, ref)
        parser.attributes(file_path)
      end

714
      def languages(ref = nil)
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
715 716
        wrapped_gitaly_errors do
          gitaly_commit_client.languages(ref)
717 718 719
        end
      end

720
      def license_short_name
721 722
        wrapped_gitaly_errors do
          gitaly_repository_client.license_short_name
723 724 725
        end
      end

726
      def fetch_source_branch!(source_repository, source_branch, local_ref)
727 728
        wrapped_gitaly_errors do
          gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref)
729 730 731 732
        end
      end

      def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
733 734 735 736 737 738 739 740 741 742 743 744
        tmp_ref = "refs/tmp/#{SecureRandom.hex}"

        return unless fetch_source_branch!(source_repository, source_branch_name, tmp_ref)

        Gitlab::Git::Compare.new(
          self,
          target_branch_name,
          tmp_ref,
          straight: straight
        )
      ensure
        delete_refs(tmp_ref)
745 746
      end

747
      def write_ref(ref_path, ref, old_ref: nil)
748 749
        ref_path = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref_path}" unless ref_path.start_with?("refs/") || ref_path == "HEAD"

750
        wrapped_gitaly_errors do
751
          gitaly_repository_client.write_ref(ref_path, ref, old_ref)
752 753 754
        end
      end

755 756 757 758 759
      # Refactoring aid; allows us to copy code from app/models/repository.rb
      def commit(ref = 'HEAD')
        Gitlab::Git::Commit.find(self, ref)
      end

760 761
      def empty?
        !has_visible_content?
762 763
      end

764
      def fetch_repository_as_mirror(repository)
765 766
        wrapped_gitaly_errors do
          gitaly_remote_client.fetch_internal_remote(repository)
767
        end
768 769
      end

770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
      # Fetch remote for repository
      #
      # remote - remote name
      # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication
      # forced - should we use --force flag?
      # no_tags - should we use --no-tags flag?
      # prune - should we use --prune flag?
      def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
        wrapped_gitaly_errors do
          gitaly_repository_client.fetch_remote(
            remote,
            ssh_auth: ssh_auth,
            forced: forced,
            no_tags: no_tags,
            prune: prune,
            timeout: GITLAB_PROJECTS_TIMEOUT
          )
        end
      end

790 791 792 793
      def blob_at(sha, path)
        Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha)
      end

794
      # Items should be of format [[commit_id, path], [commit_id1, path1]]
795
      def batch_blobs(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
796 797 798
        Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
      end

799
      def fsck
Kim Carlbäcker's avatar
Kim Carlbäcker committed
800
        msg, status = gitaly_repository_client.fsck
801

Kim Carlbäcker's avatar
Kim Carlbäcker committed
802
        raise GitError.new("Could not fsck repository: #{msg}") unless status.zero?
803 804
      end

805
      def create_from_bundle(bundle_path)
806 807 808 809 810
        # It's important to check that the linked-to file is actually a valid
        # .bundle file as it is passed to `git clone`, which may otherwise
        # interpret it as a pointer to another repository
        ::Gitlab::Git::BundleFile.check!(bundle_path)

811
        gitaly_repository_client.create_from_bundle(bundle_path)
812 813
      end

814 815 816 817
      def create_from_snapshot(url, auth)
        gitaly_repository_client.create_from_snapshot(url, auth)
      end

818
      def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
819 820 821 822 823 824
        wrapped_gitaly_errors do
          gitaly_operation_client.user_rebase(user, rebase_id,
                                            branch: branch,
                                            branch_sha: branch_sha,
                                            remote_repository: remote_repository,
                                            remote_branch: remote_branch)
825 826 827 828
        end
      end

      def rebase_in_progress?(rebase_id)
829 830
        wrapped_gitaly_errors do
          gitaly_repository_client.rebase_in_progress?(rebase_id)
831
        end
832 833 834
      end

      def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
835 836
        wrapped_gitaly_errors do
          gitaly_operation_client.user_squash(user, squash_id, branch,
837
              start_sha, end_sha, author, message)
838 839 840 841
        end
      end

      def squash_in_progress?(squash_id)
842 843
        wrapped_gitaly_errors do
          gitaly_repository_client.squash_in_progress?(squash_id)
844
        end
845 846
      end

847
      def bundle_to_disk(save_path)
848 849
        wrapped_gitaly_errors do
          gitaly_repository_client.create_bundle(save_path)
850 851 852 853 854
        end

        true
      end

855 856 857 858 859
      def multi_action(
        user, branch_name:, message:, actions:,
        author_email: nil, author_name: nil,
        start_branch_name: nil, start_repository: self)

860 861
        wrapped_gitaly_errors do
          gitaly_operation_client.user_commit_files(user, branch_name,
862 863
              message, actions, author_email, author_name,
              start_branch_name, start_repository)
864 865 866
        end
      end

867
      def write_config(full_path:)
868 869
        return unless full_path.present?

870
        # This guard avoids Gitaly log/error spam
871
        raise NoRepository, 'repository does not exist' unless exists?
872

873 874 875 876 877 878 879 880 881 882
        set_config('gitlab.fullpath' => full_path)
      end

      def set_config(entries)
        wrapped_gitaly_errors do
          gitaly_repository_client.set_config(entries)
        end
      end

      def delete_config(*keys)
883
        wrapped_gitaly_errors do
884
          gitaly_repository_client.delete_config(keys)
885
        end
886 887
      end

888
      def gitaly_repository
889
        Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository, @gl_project_path)
890 891
      end

892 893 894 895 896 897
      def gitaly_ref_client
        @gitaly_ref_client ||= Gitlab::GitalyClient::RefService.new(self)
      end

      def gitaly_commit_client
        @gitaly_commit_client ||= Gitlab::GitalyClient::CommitService.new(self)
898 899 900 901
      end

      def gitaly_repository_client
        @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self)
902 903
      end

904 905 906 907
      def gitaly_operation_client
        @gitaly_operation_client ||= Gitlab::GitalyClient::OperationService.new(self)
      end

908 909 910 911
      def gitaly_remote_client
        @gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self)
      end

912 913 914 915
      def gitaly_blob_client
        @gitaly_blob_client ||= Gitlab::GitalyClient::BlobService.new(self)
      end

916 917
      def gitaly_conflicts_client(our_commit_oid, their_commit_oid)
        Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid)
918 919
      end

920
      def clean_stale_repository_files
921 922
        wrapped_gitaly_errors do
          gitaly_repository_client.cleanup if exists?
923 924
        end
      rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup
925
        Rails.logger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}")
926 927 928 929 930 931
        Gitlab::Metrics.counter(
          :failed_repository_cleanup_total,
          'Number of failed repository cleanup events'
        ).increment
      end

932
      def branch_names_contains_sha(sha)
933
        gitaly_ref_client.branch_names_contains_sha(sha)
934
      end
935

936
      def tag_names_contains_sha(sha)
937
        gitaly_ref_client.tag_names_contains_sha(sha)
938 939 940 941 942
      end

      def search_files_by_content(query, ref)
        return [] if empty? || query.blank?

943 944 945
        safe_query = Regexp.escape(query)
        ref ||= root_ref

946
        gitaly_repository_client.search_files_by_content(ref, safe_query)
947 948
      end

949
      def can_be_merged?(source_sha, target_branch)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
950
        if target_sha = find_branch(target_branch)&.target
951 952 953 954
          !gitaly_conflicts_client(source_sha, target_sha).conflicts?
        else
          false
        end
955 956
      end

957
      def search_files_by_name(query, ref)
958
        safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
959
        ref ||= root_ref
960 961 962

        return [] if empty? || safe_query.blank?

963
        gitaly_repository_client.search_files_by_name(ref, safe_query)
964 965 966
      end

      def find_commits_by_message(query, ref, path, limit, offset)
967 968 969 970
        wrapped_gitaly_errors do
          gitaly_commit_client
            .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
            .map { |c| commit(c) }
971 972 973
        end
      end

974 975 976 977 978 979
      def list_last_commits_for_tree(sha, path, offset: 0, limit: 25)
        wrapped_gitaly_errors do
          gitaly_commit_client.list_last_commits_for_tree(sha, path, offset: offset, limit: limit)
        end
      end

980
      def last_commit_for_path(sha, path)
981 982
        wrapped_gitaly_errors do
          gitaly_commit_client.last_commit_for_path(sha, path)
983 984 985
        end
      end

986
      def checksum
987 988 989 990 991 992
        # The exists? RPC is much cheaper, so we perform this request first
        raise NoRepository, "Repository does not exists" unless exists?

        gitaly_repository_client.calculate_checksum
      rescue GRPC::NotFound
        raise NoRepository # Guard against data races.
993 994
      end

Robert Speicher's avatar
Robert Speicher committed
995 996
      private

997 998 999 1000
      def empty_diff_stats
        Gitlab::Git::DiffStatsCollection.new([])
      end

1001
      def uncached_has_local_branches?
1002 1003
        wrapped_gitaly_errors do
          gitaly_repository_client.has_local_branches?
1004 1005 1006
        end
      end

1007 1008 1009 1010 1011 1012 1013 1014
      def gitaly_merged_branch_names(branch_names, root_sha)
        qualified_branch_names = branch_names.map { |b| "refs/heads/#{b}" }

        gitaly_ref_client.merged_branches(qualified_branch_names)
          .reject { |b| b.target == root_sha }
          .map(&:name)
      end

1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
      def process_count_commits_options(options)
        if options[:from] || options[:to]
          ref =
            if options[:left_right] # Compare with merge-base for left-right
              "#{options[:from]}...#{options[:to]}"
            else
              "#{options[:from]}..#{options[:to]}"
            end

          options.merge(ref: ref)

        elsif options[:ref] && options[:left_right]
          from, to = options[:ref].match(/\A([^\.]*)\.{2,3}([^\.]*)\z/)[1..2]

          options.merge(from: from, to: to)
        else
          options
        end
      end

1035 1036 1037 1038 1039 1040
      def gitaly_submodule_url_for(ref, path)
        # We don't care about the contents so 1 byte is enough. Can't request 0 bytes, 0 means unlimited.
        commit_object = gitaly_commit_client.tree_entry(ref, path, 1)

        return unless commit_object && commit_object.type == :COMMIT

Clement Ho's avatar
Clement Ho committed
1041
        gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
1042 1043
        return unless gitmodules

1044 1045 1046 1047 1048
        found_module = GitmodulesParser.new(gitmodules.data).parse[path]

        found_module && found_module['url']
      end

1049 1050 1051
      # Returns true if the given ref name exists
      #
      # Ref names must start with `refs/`.
1052 1053 1054 1055
      def gitaly_ref_exists?(ref_name)
        gitaly_ref_client.ref_exists?(ref_name)
      end

1056 1057 1058 1059
      def gitaly_copy_gitattributes(revision)
        gitaly_repository_client.apply_gitattributes(revision)
      end

1060
      def gitaly_delete_refs(*ref_names)
1061
        gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
1062
      end
Robert Speicher's avatar
Robert Speicher committed
1063 1064 1065
    end
  end
end