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

R
Robert Speicher 已提交
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
14
      prepend Gitlab::Git::RuggedImpl::Repository
R
Robert Speicher 已提交
15 16

      SEARCH_CONTEXT_LINES = 3
17
      REV_LIST_COMMIT_LIMIT = 2_000
18
      GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
19
      GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
20
      EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze
R
Robert Speicher 已提交
21

22
      NoRepository = Class.new(StandardError)
23
      InvalidRepository = Class.new(StandardError)
24 25
      InvalidBlobName = Class.new(StandardError)
      InvalidRef = Class.new(StandardError)
26
      GitError = Class.new(StandardError)
27
      DeleteBranchError = Class.new(StandardError)
28
      CreateTreeError = Class.new(StandardError)
29
      TagExistsError = Class.new(StandardError)
30
      ChecksumError = Class.new(StandardError)
R
Robert Speicher 已提交
31 32 33 34

      # Directory name of repo
      attr_reader :name

35 36 37
      # Relative path of repo
      attr_reader :relative_path

38
      attr_reader :storage, :gl_repository, :relative_path, :gl_project_path
J
Jacob Vosmaer 已提交
39

40 41 42 43 44 45 46
      # 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

47 48
      # This initializer method is only used on the client side (gitlab-ce).
      # Gitaly-ruby uses a different initializer.
49
      def initialize(storage, relative_path, gl_repository, gl_project_path)
J
Jacob Vosmaer 已提交
50
        @storage = storage
51
        @relative_path = relative_path
52
        @gl_repository = gl_repository
53
        @gl_project_path = gl_project_path
54 55

        @name = @relative_path.split("/").last
R
Robert Speicher 已提交
56 57
      end

J
Jacob Vosmaer 已提交
58
      def ==(other)
59 60 61 62 63 64 65
        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
J
Jacob Vosmaer 已提交
66 67
      end

68
      # This method will be removed when Gitaly reaches v1.1.
69
      def path
70
        File.join(
71 72 73 74
          Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path
        )
      end

R
Robert Speicher 已提交
75 76
      # Default branch in the repository
      def root_ref
77 78 79 80 81
        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)
R
Robert Speicher 已提交
82 83
      end

84
      def exists?
85
        gitaly_repository_client.exists?
86 87
      end

88 89 90 91 92 93
      def create_repository
        wrapped_gitaly_errors do
          gitaly_repository_client.create_repository
        end
      end

R
Robert Speicher 已提交
94 95 96
      # Returns an Array of branch names
      # sorted by name ASC
      def branch_names
97 98
        wrapped_gitaly_errors do
          gitaly_ref_client.branch_names
99
        end
R
Robert Speicher 已提交
100 101 102
      end

      # Returns an Array of Branches
103
      def branches
104 105
        wrapped_gitaly_errors do
          gitaly_ref_client.branches
106
        end
R
Robert Speicher 已提交
107 108 109 110
      end

      # Directly find a branch with a simple name (e.g. master)
      #
J
Jacob Vosmaer 已提交
111 112 113
      def find_branch(name)
        wrapped_gitaly_errors do
          gitaly_ref_client.find_branch(name)
J
Jacob Vosmaer 已提交
114
        end
R
Robert Speicher 已提交
115 116
      end

117
      def local_branches(sort_by: nil)
118 119
        wrapped_gitaly_errors do
          gitaly_ref_client.local_branches(sort_by: sort_by)
R
Robert Speicher 已提交
120 121 122 123 124
        end
      end

      # Returns the number of valid branches
      def branch_count
J
Jacob Vosmaer 已提交
125 126
        wrapped_gitaly_errors do
          gitaly_ref_client.count_branch_names
127 128
        end
      end
R
Robert Speicher 已提交
129

130 131 132 133
      def expire_has_local_branches_cache
        clear_memoization(:has_local_branches)
      end

134
      def has_local_branches?
135 136 137 138 139
        strong_memoize(:has_local_branches) do
          uncached_has_local_branches?
        end
      end

140 141 142 143 144 145 146
      # 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?

147 148
      # Returns the number of valid tags
      def tag_count
J
Jacob Vosmaer 已提交
149 150
        wrapped_gitaly_errors do
          gitaly_ref_client.count_tag_names
R
Robert Speicher 已提交
151 152 153 154 155
        end
      end

      # Returns an Array of tag names
      def tag_names
156 157 158
        wrapped_gitaly_errors do
          gitaly_ref_client.tag_names
        end
R
Robert Speicher 已提交
159 160 161
      end

      # Returns an Array of Tags
J
Jacob Vosmaer 已提交
162
      #
R
Robert Speicher 已提交
163
      def tags
164 165
        wrapped_gitaly_errors do
          gitaly_ref_client.tags
A
Ahmad Sherif 已提交
166
        end
R
Robert Speicher 已提交
167 168
      end

169 170 171 172
      # Returns true if the given ref name exists
      #
      # Ref names must start with `refs/`.
      def ref_exists?(ref_name)
J
Jacob Vosmaer 已提交
173 174
        wrapped_gitaly_errors do
          gitaly_ref_exists?(ref_name)
175 176 177
        end
      end

R
Robert Speicher 已提交
178 179 180 181
      # Returns true if the given tag exists
      #
      # name - The name of the tag as a String.
      def tag_exists?(name)
J
Jacob Vosmaer 已提交
182 183
        wrapped_gitaly_errors do
          gitaly_ref_exists?("refs/tags/#{name}")
184
        end
R
Robert Speicher 已提交
185 186 187 188 189 190
      end

      # Returns true if the given branch exists
      #
      # name - The name of the branch as a String.
      def branch_exists?(name)
J
Jacob Vosmaer 已提交
191 192
        wrapped_gitaly_errors do
          gitaly_ref_exists?("refs/heads/#{name}")
193
        end
R
Robert Speicher 已提交
194 195 196 197 198 199 200
      end

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

201
      def delete_all_refs_except(prefixes)
J
Jacob Vosmaer 已提交
202 203
        wrapped_gitaly_errors do
          gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
204
        end
205 206
      end

207
      def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:, path: nil)
R
Robert Speicher 已提交
208 209 210 211
        ref ||= root_ref
        commit = Gitlab::Git::Commit.find(self, ref)
        return {} if commit.nil?

212
        prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha, path: path)
R
Robert Speicher 已提交
213 214 215

        {
          'ArchivePrefix' => prefix,
216
          'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format),
217 218
          'CommitId' => commit.id,
          'GitalyRepository' => gitaly_repository.to_h
R
Robert Speicher 已提交
219 220 221
        }
      end

222 223
      # 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
224
      def archive_prefix(ref, sha, project_path, append_sha:, path:)
225 226 227 228
        append_sha = (ref != sha) if append_sha.nil?

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

229
        prefix_segments = [project_path, formatted_ref]
230
        prefix_segments << sha if append_sha
231
        prefix_segments << path.tr('/', '-').gsub(%r{^/|/$}, '') if path
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251

        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")
R
Robert Speicher 已提交
252
        # Build file path
253
        return unless name
R
Robert Speicher 已提交
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268

        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}"
269
        File.join(storage_path, self.gl_repository, sha, file_name)
R
Robert Speicher 已提交
270
      end
271
      private :archive_file_path
R
Robert Speicher 已提交
272 273 274

      # Return repo size in megabytes
      def size
275
        size = gitaly_repository_client.repository_size
276

R
Robert Speicher 已提交
277 278 279
        (size.to_f / 1024).round(2)
      end

280 281 282 283 284
      # Return git object directory size in bytes
      def object_directory_size
        gitaly_repository_client.get_object_directory_size.to_f * 1024
      end

285
      # Build an array of commits.
R
Robert Speicher 已提交
286 287 288 289 290 291 292 293 294 295
      #
      # Usage.
      #   repo.log(
      #     ref: 'master',
      #     path: 'app/models',
      #     limit: 10,
      #     offset: 5,
      #     after: Time.new(2016, 4, 21, 14, 32, 10)
      #   )
      def log(options)
296 297 298 299 300 301 302
        default_options = {
          limit: 10,
          offset: 0,
          path: nil,
          follow: false,
          skip_merges: false,
          after: nil,
T
Tiago Botelho 已提交
303 304
          before: nil,
          all: false
305 306 307 308 309
        }

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

310 311 312 313 314
        limit = options[:limit]
        if limit == 0 || !limit.is_a?(Integer)
          raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
        end

315 316
        wrapped_gitaly_errors do
          gitaly_commit_client.find_commits(options)
317
        end
R
Robert Speicher 已提交
318 319
      end

320
      def new_commits(newrev)
321 322
        wrapped_gitaly_errors do
          gitaly_ref_client.list_new_commits(newrev)
323 324 325
        end
      end

326
      def new_blobs(newrev, dynamic_timeout: nil)
327
        return [] if newrev.blank? || newrev == ::Gitlab::Git::BLANK_SHA
328 329 330

        strong_memoize("new_blobs_#{newrev}") do
          wrapped_gitaly_errors do
331
            gitaly_ref_client.list_new_blobs(newrev, REV_LIST_COMMIT_LIMIT, dynamic_timeout: dynamic_timeout)
332 333 334 335
          end
        end
      end

336
      def count_commits(options)
337
        options = process_count_commits_options(options.dup)
338

339 340 341 342 343 344 345 346 347 348 349
        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]
350
          else
351
            gitaly_commit_client.commit_count(options[:ref], options)
352 353
          end
        end
354 355
      end

R
Robert Speicher 已提交
356
      # Counts the amount of commits between `from` and `to`.
357 358
      def count_commits_between(from, to, options = {})
        count_commits(from: from, to: to, **options)
R
Robert Speicher 已提交
359 360
      end

R
Rubén Dávila 已提交
361 362 363
      # 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)
364 365
        @raw_changes_between ||= {}

366 367 368
        @raw_changes_between[[old_rev, new_rev]] ||=
          begin
            return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
369

370
            wrapped_gitaly_errors do
371 372 373 374 375
              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
R
Rubén Dávila 已提交
376
          end
377 378
      rescue ArgumentError => e
        raise Gitlab::Git::Repository::GitError.new(e)
R
Rubén Dávila 已提交
379 380
      end

R
Robert Speicher 已提交
381
      # Returns the SHA of the most recent common ancestor of +from+ and +to+
382
      def merge_base(*commits)
383
        wrapped_gitaly_errors do
384
          gitaly_repository_client.find_merge_base(*commits)
385
        end
R
Robert Speicher 已提交
386 387
      end

388
      # Returns true is +from+ is direct ancestor to +to+, otherwise false
389
      def ancestor?(from, to)
390
        gitaly_commit_client.ancestor?(from, to)
391 392
      end

393
      def merged_branch_names(branch_names = [])
394 395 396 397 398 399
        return [] unless root_ref

        root_sha = find_branch(root_ref)&.target

        return [] unless root_sha

400 401
        branches = wrapped_gitaly_errors do
          gitaly_merged_branch_names(branch_names, root_sha)
402 403 404
        end

        Set.new(branches)
405 406
      end

R
Robert Speicher 已提交
407 408 409 410 411
      # 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)
412
        iterator = gitaly_commit_client.diff(from, to, options.merge(paths: paths))
413 414

        Gitlab::Git::DiffCollection.new(iterator, options)
R
Robert Speicher 已提交
415 416
      end

417
      def diff_stats(left_id, right_id)
418 419 420 421
        if [left_id, right_id].any? { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) }
          return empty_diff_stats
        end

422 423 424 425 426
        stats = wrapped_gitaly_errors do
          gitaly_commit_client.diff_stats(left_id, right_id)
        end

        Gitlab::Git::DiffStatsCollection.new(stats)
427
      rescue CommandError, TypeError
428
        empty_diff_stats
429
      end
R
Robert Speicher 已提交
430

431 432
      # Returns a RefName for a given SHA
      def ref_name_for_sha(ref_path, sha)
433 434
        raise ArgumentError, "sha can't be empty" unless sha.present?

435
        gitaly_ref_client.find_ref_name(sha, ref_path)
436 437
      end

T
Takuya Noguchi 已提交
438
      # Get refs hash which key is the commit id
439 440
      # and value is a Gitlab::Git::Tag or Gitlab::Git::Branch
      # Note that both inherit from Gitlab::Git::Ref
R
Robert Speicher 已提交
441
      def refs_hash
442 443 444 445 446
        return @refs_hash if @refs_hash

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

        (tags + branches).each do |ref|
J
John Cai 已提交
447
          next unless ref.target && ref.name && ref.dereferenced_target&.id
448 449

          @refs_hash[ref.dereferenced_target.id] << ref.name
R
Robert Speicher 已提交
450
        end
451

R
Robert Speicher 已提交
452 453 454
        @refs_hash
      end

455
      # Returns url for submodule
R
Robert Speicher 已提交
456 457
      #
      # Ex.
458 459
      #   @repository.submodule_url_for('master', 'rack')
      #   # => git@localhost:rack.git
R
Robert Speicher 已提交
460
      #
461
      def submodule_url_for(ref, path)
J
Jacob Vosmaer 已提交
462 463
        wrapped_gitaly_errors do
          gitaly_submodule_url_for(ref, path)
R
Robert Speicher 已提交
464 465 466 467 468
        end
      end

      # Return total commits count accessible from passed ref
      def commit_count(ref)
469 470
        wrapped_gitaly_errors do
          gitaly_commit_client.commit_count(ref)
471
        end
R
Robert Speicher 已提交
472 473
      end

474
      # Return total diverging commits count
475
      def diverging_commit_count(from, to, max_count: 0)
476 477 478 479 480
        wrapped_gitaly_errors do
          gitaly_commit_client.diverging_commit_count(from, to, max_count: max_count)
        end
      end

R
Robert Speicher 已提交
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
      # 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

505
      def add_branch(branch_name, user:, target:)
506 507 508
        wrapped_gitaly_errors do
          gitaly_operation_client.user_create_branch(branch_name, user, target)
        end
509 510
      end

511
      def add_tag(tag_name, user:, target:, message: nil)
512 513
        wrapped_gitaly_errors do
          gitaly_operation_client.add_tag(tag_name, user, target, message)
514 515 516
        end
      end

517
      def update_branch(branch_name, user:, newrev:, oldrev:)
518 519
        wrapped_gitaly_errors do
          gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
520
        end
521 522
      end

523
      def rm_branch(branch_name, user:)
524 525
        wrapped_gitaly_errors do
          gitaly_operation_client.user_delete_branch(branch_name, user)
526
        end
527 528
      end

529
      def rm_tag(tag_name, user:)
530 531
        wrapped_gitaly_errors do
          gitaly_operation_client.rm_tag(tag_name, user)
532
        end
533 534 535 536 537 538
      end

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

539
      def merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
540
        wrapped_gitaly_errors do
541
          gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
542 543 544
        end
      end

J
Jacob Vosmaer 已提交
545
      def merge(user, source_sha, target_branch, message, &block)
546 547
        wrapped_gitaly_errors do
          gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block)
548 549 550
        end
      end

551
      def ff_merge(user, source_sha, target_branch)
552 553
        wrapped_gitaly_errors do
          gitaly_operation_client.user_ff_branch(user, source_sha, target_branch)
554 555 556
        end
      end

557
      def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
558 559 560 561 562 563 564 565
        args = {
          user: user,
          commit: commit,
          branch_name: branch_name,
          message: message,
          start_branch_name: start_branch_name,
          start_repository: start_repository
        }
566

567 568
        wrapped_gitaly_errors do
          gitaly_operation_client.user_revert(args)
569 570 571 572
        end
      end

      def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
573 574 575 576 577 578 579 580
        args = {
          user: user,
          commit: commit,
          branch_name: branch_name,
          message: message,
          start_branch_name: start_branch_name,
          start_repository: start_repository
        }
581

582 583
        wrapped_gitaly_errors do
          gitaly_operation_client.user_cherry_pick(args)
584 585 586
        end
      end

587 588 589 590 591 592 593 594 595 596 597 598 599 600
      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

R
Robert Speicher 已提交
601 602
      # Delete the specified branch from the repository
      def delete_branch(branch_name)
J
Jacob Vosmaer 已提交
603 604
        wrapped_gitaly_errors do
          gitaly_ref_client.delete_branch(branch_name)
605
        end
J
Jacob Vosmaer 已提交
606
      rescue CommandError => e
607
        raise DeleteBranchError, e
R
Robert Speicher 已提交
608
      end
L
Lin Jen-Shin 已提交
609

610
      def delete_refs(*ref_names)
J
Jacob Vosmaer 已提交
611 612
        wrapped_gitaly_errors do
          gitaly_delete_refs(*ref_names)
613
        end
L
Lin Jen-Shin 已提交
614
      end
R
Robert Speicher 已提交
615 616 617 618 619 620 621

      # 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")
J
Jacob Vosmaer 已提交
622 623
        wrapped_gitaly_errors do
          gitaly_ref_client.create_branch(ref, start_point)
624
        end
R
Robert Speicher 已提交
625 626
      end

627 628
      # If `mirror_refmap` is present the remote is set as mirror with that mapping
      def add_remote(remote_name, url, mirror_refmap: nil)
629 630
        wrapped_gitaly_errors do
          gitaly_remote_client.add_remote(remote_name, url, mirror_refmap)
631
        end
R
Robert Speicher 已提交
632 633
      end

634
      def remove_remote(remote_name)
635 636
        wrapped_gitaly_errors do
          gitaly_remote_client.remove_remote(remote_name)
637
        end
638 639
      end

640 641 642 643 644 645 646 647
      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

R
Robert Speicher 已提交
648 649 650 651 652 653
      # Returns result like "git ls-files" , recursive and full file path
      #
      # Ex.
      #   repo.ls_files('master')
      #
      def ls_files(ref)
654
        gitaly_commit_client.ls_files(ref)
R
Robert Speicher 已提交
655 656 657
      end

      def copy_gitattributes(ref)
J
Jacob Vosmaer 已提交
658 659
        wrapped_gitaly_errors do
          gitaly_repository_client.apply_gitattributes(ref)
R
Robert Speicher 已提交
660 661 662
        end
      end

663 664 665
      def info_attributes
        return @info_attributes if @info_attributes

666
        content = gitaly_repository_client.info_attributes
667 668 669
        @info_attributes = AttributesParser.new(content)
      end

R
Robert Speicher 已提交
670 671 672 673
      # Returns the Git attributes for the given file path.
      #
      # See `Gitlab::Git::Attributes` for more information.
      def attributes(path)
674
        info_attributes.attributes(path)
R
Robert Speicher 已提交
675 676
      end

S
Sean McGivern 已提交
677 678 679 680
      def gitattribute(path, name)
        attributes(path)[name]
      end

681
      # Returns parsed .gitattributes for a given ref
682
      #
683
      # This only parses the root .gitattributes file,
684 685
      # it does not traverse subfolders to find additional .gitattributes files
      #
686 687 688
      # 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.
689 690
      def attributes_at(ref)
        AttributesAtRefParser.new(self, ref)
691 692
      end

693
      def languages(ref = nil)
Z
Zeger-Jan van de Weg 已提交
694 695
        wrapped_gitaly_errors do
          gitaly_commit_client.languages(ref)
696 697 698
        end
      end

699
      def license_short_name
700 701
        wrapped_gitaly_errors do
          gitaly_repository_client.license_short_name
702 703 704
        end
      end

705
      def fetch_source_branch!(source_repository, source_branch, local_ref)
706 707
        wrapped_gitaly_errors do
          gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref)
708 709 710 711
        end
      end

      def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
        reachable_ref =
          if source_repository == self
            source_branch_name
          else
            # If a tmp ref was created before for a separate repo comparison (forks),
            # we're able to short-circuit the tmp ref re-creation:
            # 1. Take the SHA from the source repo
            # 2. Read that in the current "target" repo
            # 3. If that SHA is still known (readable), it means GC hasn't
            # cleaned it up yet, so we can use it instead re-writing the tmp ref.
            source_commit_id = source_repository.commit(source_branch_name)&.sha
            commit(source_commit_id)&.sha if source_commit_id
          end

        return compare(target_branch_name, reachable_ref, straight: straight) if reachable_ref
727

728 729 730 731
        tmp_ref = "refs/tmp/#{SecureRandom.hex}"

        return unless fetch_source_branch!(source_repository, source_branch_name, tmp_ref)

732
        compare(target_branch_name, tmp_ref, straight: straight)
733
      ensure
734
        delete_refs(tmp_ref) if tmp_ref
735 736
      end

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

740
        wrapped_gitaly_errors do
741
          gitaly_repository_client.write_ref(ref_path, ref, old_ref)
742 743 744
        end
      end

745 746 747 748 749
      # Refactoring aid; allows us to copy code from app/models/repository.rb
      def commit(ref = 'HEAD')
        Gitlab::Git::Commit.find(self, ref)
      end

750 751
      def empty?
        !has_visible_content?
752 753
      end

754
      def fetch_repository_as_mirror(repository)
755 756
        wrapped_gitaly_errors do
          gitaly_remote_client.fetch_internal_remote(repository)
757
        end
758 759
      end

760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
      # 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

780 781 782 783
      def blob_at(sha, path)
        Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha)
      end

784
      # Items should be of format [[commit_id, path], [commit_id1, path1]]
785
      def batch_blobs(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
786 787 788
        Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
      end

789
      def fsck
790
        msg, status = gitaly_repository_client.fsck
791

792
        raise GitError.new("Could not fsck repository: #{msg}") unless status.zero?
793 794
      end

795
      def create_from_bundle(bundle_path)
796 797 798 799 800
        # 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)

801
        gitaly_repository_client.create_from_bundle(bundle_path)
802 803
      end

804 805 806 807
      def create_from_snapshot(url, auth)
        gitaly_repository_client.create_from_snapshot(url, auth)
      end

808 809
      # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628
      def rebase_deprecated(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
810 811 812 813 814 815
        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)
816 817 818
        end
      end

819 820 821 822 823 824 825 826 827 828 829 830 831 832
      def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, &block)
        wrapped_gitaly_errors do
          gitaly_operation_client.rebase(
            user,
            rebase_id,
            branch: branch,
            branch_sha: branch_sha,
            remote_repository: remote_repository,
            remote_branch: remote_branch,
            &block
          )
        end
      end

833
      def rebase_in_progress?(rebase_id)
834 835
        wrapped_gitaly_errors do
          gitaly_repository_client.rebase_in_progress?(rebase_id)
836
        end
837 838 839
      end

      def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
840 841
        wrapped_gitaly_errors do
          gitaly_operation_client.user_squash(user, squash_id, branch,
842
              start_sha, end_sha, author, message)
843 844 845 846
        end
      end

      def squash_in_progress?(squash_id)
847 848
        wrapped_gitaly_errors do
          gitaly_repository_client.squash_in_progress?(squash_id)
849
        end
850 851
      end

852
      def bundle_to_disk(save_path)
853 854
        wrapped_gitaly_errors do
          gitaly_repository_client.create_bundle(save_path)
855 856 857 858 859
        end

        true
      end

860
      # rubocop:disable Metrics/ParameterLists
861 862 863
      def multi_action(
        user, branch_name:, message:, actions:,
        author_email: nil, author_name: nil,
864 865
        start_branch_name: nil, start_repository: self,
        force: false)
866

867 868
        wrapped_gitaly_errors do
          gitaly_operation_client.user_commit_files(user, branch_name,
869
              message, actions, author_email, author_name,
870
              start_branch_name, start_repository, force)
871 872
        end
      end
873
      # rubocop:enable Metrics/ParameterLists
874

875
      def write_config(full_path:)
876 877
        return unless full_path.present?

878
        # This guard avoids Gitaly log/error spam
Z
Zeger-Jan van de Weg 已提交
879
        raise NoRepository, 'repository does not exist' unless exists?
880

881 882 883 884 885 886 887 888 889 890
        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)
Z
Zeger-Jan van de Weg 已提交
891
        wrapped_gitaly_errors do
892
          gitaly_repository_client.delete_config(keys)
893
        end
894 895
      end

896 897 898 899 900 901
      def disconnect_alternates
        wrapped_gitaly_errors do
          gitaly_repository_client.disconnect_alternates
        end
      end

902
      def gitaly_repository
903
        Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository, @gl_project_path)
904 905
      end

906 907 908 909 910 911
      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)
912 913 914 915
      end

      def gitaly_repository_client
        @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self)
916 917
      end

918 919 920 921
      def gitaly_operation_client
        @gitaly_operation_client ||= Gitlab::GitalyClient::OperationService.new(self)
      end

922 923 924 925
      def gitaly_remote_client
        @gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self)
      end

926 927 928 929
      def gitaly_blob_client
        @gitaly_blob_client ||= Gitlab::GitalyClient::BlobService.new(self)
      end

930 931
      def gitaly_conflicts_client(our_commit_oid, their_commit_oid)
        Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid)
932 933
      end

934
      def clean_stale_repository_files
935 936
        wrapped_gitaly_errors do
          gitaly_repository_client.cleanup if exists?
937 938
        end
      rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup
939
        Rails.logger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}")
940 941 942 943 944 945
        Gitlab::Metrics.counter(
          :failed_repository_cleanup_total,
          'Number of failed repository cleanup events'
        ).increment
      end

946
      def branch_names_contains_sha(sha)
947
        gitaly_ref_client.branch_names_contains_sha(sha)
948
      end
949

950
      def tag_names_contains_sha(sha)
951
        gitaly_ref_client.tag_names_contains_sha(sha)
952 953 954 955 956
      end

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

957 958 959
        safe_query = Regexp.escape(query)
        ref ||= root_ref

960
        gitaly_repository_client.search_files_by_content(ref, safe_query)
961 962
      end

963
      def can_be_merged?(source_sha, target_branch)
J
Jacob Vosmaer 已提交
964
        if target_sha = find_branch(target_branch)&.target
M
Mark Chao 已提交
965 966 967 968
          !gitaly_conflicts_client(source_sha, target_sha).conflicts?
        else
          false
        end
969 970
      end

971
      def search_files_by_name(query, ref)
972
        safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
973
        ref ||= root_ref
974 975 976

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

977
        gitaly_repository_client.search_files_by_name(ref, safe_query)
978 979 980
      end

      def find_commits_by_message(query, ref, path, limit, offset)
981 982 983 984
        wrapped_gitaly_errors do
          gitaly_commit_client
            .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
            .map { |c| commit(c) }
985 986 987
        end
      end

988 989 990 991 992 993
      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

994
      def last_commit_for_path(sha, path)
995 996
        wrapped_gitaly_errors do
          gitaly_commit_client.last_commit_for_path(sha, path)
997 998 999
        end
      end

1000
      def checksum
1001 1002 1003 1004 1005 1006
        # 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.
1007 1008
      end

R
Robert Speicher 已提交
1009 1010
      private

1011 1012 1013 1014 1015 1016 1017
      def compare(base_ref, head_ref, straight:)
        Gitlab::Git::Compare.new(self,
                                 base_ref,
                                 head_ref,
                                 straight: straight)
      end

1018 1019 1020 1021
      def empty_diff_stats
        Gitlab::Git::DiffStatsCollection.new([])
      end

1022
      def uncached_has_local_branches?
1023 1024
        wrapped_gitaly_errors do
          gitaly_repository_client.has_local_branches?
1025 1026 1027
        end
      end

1028 1029 1030 1031 1032 1033 1034 1035
      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

1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055
      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

1056 1057 1058 1059 1060 1061
      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

C
Clement Ho 已提交
1062
        gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
1063 1064
        return unless gitmodules

1065 1066 1067 1068 1069
        found_module = GitmodulesParser.new(gitmodules.data).parse[path]

        found_module && found_module['url']
      end

1070 1071 1072
      # Returns true if the given ref name exists
      #
      # Ref names must start with `refs/`.
1073 1074 1075 1076
      def gitaly_ref_exists?(ref_name)
        gitaly_ref_client.ref_exists?(ref_name)
      end

1077 1078 1079 1080
      def gitaly_copy_gitattributes(revision)
        gitaly_repository_client.apply_gitattributes(revision)
      end

1081
      def gitaly_delete_refs(*ref_names)
1082
        gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
1083
      end
R
Robert Speicher 已提交
1084 1085 1086
    end
  end
end