1# frozen_string_literal: true 2 3require 'tempfile' 4require 'forwardable' 5require "rubygems/package" 6 7module Gitlab 8 module Git 9 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 15 16 SEARCH_CONTEXT_LINES = 3 17 REV_LIST_COMMIT_LIMIT = 2_000 18 GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git' 19 GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout 20 EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000' 21 22 NoRepository = Class.new(::Gitlab::Git::BaseError) 23 RepositoryExists = Class.new(::Gitlab::Git::BaseError) 24 InvalidRepository = Class.new(::Gitlab::Git::BaseError) 25 InvalidBlobName = Class.new(::Gitlab::Git::BaseError) 26 InvalidRef = Class.new(::Gitlab::Git::BaseError) 27 GitError = Class.new(::Gitlab::Git::BaseError) 28 DeleteBranchError = Class.new(::Gitlab::Git::BaseError) 29 TagExistsError = Class.new(::Gitlab::Git::BaseError) 30 ChecksumError = Class.new(::Gitlab::Git::BaseError) 31 class CreateTreeError < ::Gitlab::Git::BaseError 32 attr_reader :error_code 33 34 def initialize(error_code) 35 super(self.class.name) 36 37 # The value coming from Gitaly is an uppercase String (e.g., "EMPTY") 38 @error_code = error_code.downcase.to_sym 39 end 40 end 41 42 # Directory name of repo 43 attr_reader :name 44 45 # Relative path of repo 46 attr_reader :relative_path 47 48 attr_reader :storage, :gl_repository, :gl_project_path 49 50 # This remote name has to be stable for all types of repositories that 51 # can join an object pool. If it's structure ever changes, a migration 52 # has to be performed on the object pools to update the remote names. 53 # Else the pool can't be updated anymore and is left in an inconsistent 54 # state. 55 alias_method :object_pool_remote_name, :gl_repository 56 57 # This initializer method is only used on the client side (gitlab-ce). 58 # Gitaly-ruby uses a different initializer. 59 def initialize(storage, relative_path, gl_repository, gl_project_path) 60 @storage = storage 61 @relative_path = relative_path 62 @gl_repository = gl_repository 63 @gl_project_path = gl_project_path 64 65 @name = @relative_path.split("/").last 66 end 67 68 def to_s 69 "<#{self.class.name}: #{self.gl_project_path}>" 70 end 71 72 def ==(other) 73 other.is_a?(self.class) && [storage, relative_path] == [other.storage, other.relative_path] 74 end 75 76 alias_method :eql?, :== 77 78 def hash 79 [self.class, storage, relative_path].hash 80 end 81 82 # This method will be removed when Gitaly reaches v1.1. 83 def path 84 File.join( 85 Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path 86 ) 87 end 88 89 # Default branch in the repository 90 def root_ref 91 gitaly_ref_client.default_branch_name 92 rescue GRPC::NotFound => e 93 raise NoRepository, e.message 94 rescue GRPC::Unknown => e 95 raise Gitlab::Git::CommandError, e.message 96 end 97 98 def exists? 99 gitaly_repository_client.exists? 100 end 101 102 def create_repository 103 wrapped_gitaly_errors do 104 gitaly_repository_client.create_repository 105 rescue GRPC::AlreadyExists => e 106 raise RepositoryExists, e.message 107 end 108 end 109 110 # Returns an Array of branch names 111 # sorted by name ASC 112 def branch_names 113 wrapped_gitaly_errors do 114 gitaly_ref_client.branch_names 115 end 116 end 117 118 # Returns an Array of Branches 119 def branches 120 wrapped_gitaly_errors do 121 gitaly_ref_client.branches 122 end 123 end 124 125 # Directly find a branch with a simple name (e.g. master) 126 # 127 def find_branch(name) 128 wrapped_gitaly_errors do 129 gitaly_ref_client.find_branch(name) 130 end 131 end 132 133 def find_tag(name) 134 wrapped_gitaly_errors do 135 gitaly_ref_client.find_tag(name) 136 end 137 rescue CommandError 138 end 139 140 def local_branches(sort_by: nil, pagination_params: nil) 141 wrapped_gitaly_errors do 142 gitaly_ref_client.local_branches(sort_by: sort_by, pagination_params: pagination_params) 143 end 144 end 145 146 # Returns the number of valid branches 147 def branch_count 148 wrapped_gitaly_errors do 149 gitaly_ref_client.count_branch_names 150 end 151 end 152 153 def rename(new_relative_path) 154 wrapped_gitaly_errors do 155 gitaly_repository_client.rename(new_relative_path) 156 end 157 end 158 159 def remove 160 wrapped_gitaly_errors do 161 gitaly_repository_client.remove 162 end 163 rescue NoRepository 164 nil 165 end 166 167 def replicate(source_repository) 168 wrapped_gitaly_errors do 169 gitaly_repository_client.replicate(source_repository) 170 end 171 end 172 173 def expire_has_local_branches_cache 174 clear_memoization(:has_local_branches) 175 end 176 177 def has_local_branches? 178 strong_memoize(:has_local_branches) do 179 uncached_has_local_branches? 180 end 181 end 182 183 # Git repository can contains some hidden refs like: 184 # /refs/notes/* 185 # /refs/git-as-svn/* 186 # /refs/pulls/* 187 # This refs by default not visible in project page and not cloned to client side. 188 alias_method :has_visible_content?, :has_local_branches? 189 190 # Returns the number of valid tags 191 def tag_count 192 wrapped_gitaly_errors do 193 gitaly_ref_client.count_tag_names 194 end 195 end 196 197 # Returns an Array of tag names 198 def tag_names 199 wrapped_gitaly_errors do 200 gitaly_ref_client.tag_names 201 end 202 end 203 204 # Returns an Array of Tags 205 # 206 def tags(sort_by: nil, pagination_params: nil) 207 wrapped_gitaly_errors do 208 gitaly_ref_client.tags(sort_by: sort_by, pagination_params: pagination_params) 209 end 210 end 211 212 # Returns true if the given ref name exists 213 # 214 # Ref names must start with `refs/`. 215 def ref_exists?(ref_name) 216 wrapped_gitaly_errors do 217 gitaly_ref_exists?(ref_name) 218 end 219 end 220 221 # Returns true if the given tag exists 222 # 223 # name - The name of the tag as a String. 224 def tag_exists?(name) 225 wrapped_gitaly_errors do 226 gitaly_ref_exists?("refs/tags/#{name}") 227 end 228 end 229 230 # Returns true if the given branch exists 231 # 232 # name - The name of the branch as a String. 233 def branch_exists?(name) 234 wrapped_gitaly_errors do 235 gitaly_ref_exists?("refs/heads/#{name}") 236 end 237 end 238 239 # Returns an Array of branch and tag names 240 def ref_names 241 branch_names + tag_names 242 end 243 244 def delete_all_refs_except(prefixes) 245 wrapped_gitaly_errors do 246 gitaly_ref_client.delete_refs(except_with_prefixes: prefixes) 247 end 248 end 249 250 def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:, path: nil) 251 ref ||= root_ref 252 commit = Gitlab::Git::Commit.find(self, ref) 253 return {} if commit.nil? 254 255 prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha, path: path) 256 257 { 258 'ArchivePrefix' => prefix, 259 'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format), 260 'CommitId' => commit.id, 261 'GitalyRepository' => gitaly_repository.to_h 262 } 263 end 264 265 # This is both the filename of the archive (missing the extension) and the 266 # name of the top-level member of the archive under which all files go 267 def archive_prefix(ref, sha, project_path, append_sha:, path:) 268 append_sha = (ref != sha) if append_sha.nil? 269 270 formatted_ref = ref.tr('/', '-') 271 272 prefix_segments = [project_path, formatted_ref] 273 prefix_segments << sha if append_sha 274 prefix_segments << path.tr('/', '-').gsub(%r{^/|/$}, '') if path 275 276 prefix_segments.join('-') 277 end 278 private :archive_prefix 279 280 # The full path on disk where the archive should be stored. This is used 281 # to cache the archive between requests. 282 # 283 # The path is a global namespace, so needs to be globally unique. This is 284 # achieved by including `gl_repository` in the path. 285 # 286 # Archives relating to a particular ref when the SHA is not present in the 287 # filename must be invalidated when the ref is updated to point to a new 288 # SHA. This is achieved by including the SHA in the path. 289 # 290 # As this is a full path on disk, it is not "cloud native". This should 291 # be resolved by either removing the cache, or moving the implementation 292 # into Gitaly and removing the ArchivePath parameter from the git-archive 293 # senddata response. 294 def archive_file_path(storage_path, sha, name, format = "tar.gz") 295 # Build file path 296 return unless name 297 298 extension = 299 case format 300 when "tar.bz2", "tbz", "tbz2", "tb2", "bz2" 301 "tar.bz2" 302 when "tar" 303 "tar" 304 when "zip" 305 "zip" 306 else 307 # everything else should fall back to tar.gz 308 "tar.gz" 309 end 310 311 file_name = "#{name}.#{extension}" 312 File.join(storage_path, self.gl_repository, sha, archive_version_path, file_name) 313 end 314 private :archive_file_path 315 316 def archive_version_path 317 '@v2' 318 end 319 private :archive_version_path 320 321 # Return repo size in megabytes 322 def size 323 size = gitaly_repository_client.repository_size 324 325 (size.to_f / 1024).round(2) 326 end 327 328 # Return git object directory size in bytes 329 def object_directory_size 330 gitaly_repository_client.get_object_directory_size.to_f * 1024 331 end 332 333 # Build an array of commits. 334 # 335 # Usage. 336 # repo.log( 337 # ref: 'master', 338 # path: 'app/models', 339 # limit: 10, 340 # offset: 5, 341 # after: Time.new(2016, 4, 21, 14, 32, 10) 342 # ) 343 def log(options) 344 default_options = { 345 limit: 10, 346 offset: 0, 347 path: nil, 348 author: nil, 349 follow: false, 350 skip_merges: false, 351 after: nil, 352 before: nil, 353 all: false 354 } 355 356 options = default_options.merge(options) 357 options[:offset] ||= 0 358 359 limit = options[:limit] 360 if limit == 0 || !limit.is_a?(Integer) 361 raise ArgumentError, "invalid Repository#log limit: #{limit.inspect}" 362 end 363 364 wrapped_gitaly_errors do 365 gitaly_commit_client.find_commits(options) 366 end 367 end 368 369 def new_commits(newrevs, allow_quarantine: false) 370 wrapped_gitaly_errors do 371 gitaly_commit_client.list_new_commits(Array.wrap(newrevs), allow_quarantine: allow_quarantine) 372 end 373 end 374 375 def new_blobs(newrevs, dynamic_timeout: nil) 376 newrevs = Array.wrap(newrevs).reject { |rev| rev.blank? || rev == ::Gitlab::Git::BLANK_SHA } 377 return [] if newrevs.empty? 378 379 newrevs = newrevs.uniq.sort 380 381 @new_blobs ||= Hash.new do |h, revs| 382 h[revs] = blobs(['--not', '--all', '--not'] + newrevs, with_paths: true, dynamic_timeout: dynamic_timeout) 383 end 384 385 @new_blobs[newrevs] 386 end 387 388 # List blobs reachable via a set of revisions. Supports the 389 # pseudo-revisions `--not` and `--all`. Uses the minimum of 390 # GitalyClient.medium_timeout and dynamic timeout if the dynamic 391 # timeout is set, otherwise it'll always use the medium timeout. 392 def blobs(revisions, with_paths: false, dynamic_timeout: nil) 393 revisions = revisions.reject { |rev| rev.blank? || rev == ::Gitlab::Git::BLANK_SHA } 394 395 return [] if revisions.blank? 396 397 wrapped_gitaly_errors do 398 gitaly_blob_client.list_blobs(revisions, limit: REV_LIST_COMMIT_LIMIT, 399 with_paths: with_paths, dynamic_timeout: dynamic_timeout) 400 end 401 end 402 403 def count_commits(options) 404 options = process_count_commits_options(options.dup) 405 406 wrapped_gitaly_errors do 407 if options[:left_right] 408 from = options[:from] 409 to = options[:to] 410 411 right_count = gitaly_commit_client 412 .commit_count("#{from}..#{to}", options) 413 left_count = gitaly_commit_client 414 .commit_count("#{to}..#{from}", options) 415 416 [left_count, right_count] 417 else 418 gitaly_commit_client.commit_count(options[:ref], options) 419 end 420 end 421 end 422 423 # Counts the amount of commits between `from` and `to`. 424 def count_commits_between(from, to, options = {}) 425 count_commits(from: from, to: to, **options) 426 end 427 428 # old_rev and new_rev are commit ID's 429 # the result of this method is an array of Gitlab::Git::RawDiffChange 430 def raw_changes_between(old_rev, new_rev) 431 @raw_changes_between ||= {} 432 433 @raw_changes_between[[old_rev, new_rev]] ||= 434 begin 435 return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA 436 437 wrapped_gitaly_errors do 438 gitaly_repository_client.raw_changes_between(old_rev, new_rev) 439 .each_with_object([]) do |msg, arr| 440 msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) } 441 end 442 end 443 end 444 rescue ArgumentError => e 445 raise Gitlab::Git::Repository::GitError, e 446 end 447 448 # Returns the SHA of the most recent common ancestor of +from+ and +to+ 449 def merge_base(*commits) 450 wrapped_gitaly_errors do 451 gitaly_repository_client.find_merge_base(*commits) 452 end 453 end 454 455 # Returns true is +from+ is direct ancestor to +to+, otherwise false 456 def ancestor?(from, to) 457 gitaly_commit_client.ancestor?(from, to) 458 end 459 460 def merged_branch_names(branch_names = []) 461 return [] unless root_ref 462 463 root_sha = find_branch(root_ref)&.target 464 465 return [] unless root_sha 466 467 branches = wrapped_gitaly_errors do 468 gitaly_merged_branch_names(branch_names, root_sha) 469 end 470 471 Set.new(branches) 472 end 473 474 # Return an array of Diff objects that represent the diff 475 # between +from+ and +to+. See Diff::filter_diff_options for the allowed 476 # diff options. The +options+ hash can also include :break_rewrites to 477 # split larger rewrites into delete/add pairs. 478 def diff(from, to, options = {}, *paths) 479 iterator = gitaly_commit_client.diff(from, to, options.merge(paths: paths)) 480 481 Gitlab::Git::DiffCollection.new(iterator, options) 482 end 483 484 def diff_stats(left_id, right_id) 485 if [left_id, right_id].any? { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) } 486 return empty_diff_stats 487 end 488 489 stats = wrapped_gitaly_errors do 490 gitaly_commit_client.diff_stats(left_id, right_id) 491 end 492 493 Gitlab::Git::DiffStatsCollection.new(stats) 494 rescue CommandError, TypeError 495 empty_diff_stats 496 end 497 498 def find_changed_paths(commits) 499 processed_commits = commits.reject { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) } 500 501 return [] if processed_commits.empty? 502 503 wrapped_gitaly_errors do 504 gitaly_commit_client.find_changed_paths(processed_commits) 505 end 506 rescue CommandError, TypeError, NoRepository 507 [] 508 end 509 510 # Get refs hash which key is the commit id 511 # and value is a Gitlab::Git::Tag or Gitlab::Git::Branch 512 # Note that both inherit from Gitlab::Git::Ref 513 def refs_hash 514 return @refs_hash if @refs_hash 515 516 @refs_hash = Hash.new { |h, k| h[k] = [] } 517 518 (tags + branches).each do |ref| 519 next unless ref.target && ref.name && ref.dereferenced_target&.id 520 521 @refs_hash[ref.dereferenced_target.id] << ref.name 522 end 523 524 @refs_hash 525 end 526 527 # Returns matching refs for OID 528 # 529 # Limit of 0 means there is no limit. 530 def refs_by_oid(oid:, limit: 0) 531 wrapped_gitaly_errors do 532 gitaly_ref_client.find_refs_by_oid(oid: oid, limit: limit) 533 end 534 rescue CommandError, TypeError, NoRepository 535 nil 536 end 537 538 # Returns url for submodule 539 # 540 # Ex. 541 # @repository.submodule_url_for('master', 'rack') 542 # # => git@localhost:rack.git 543 # 544 def submodule_url_for(ref, path) 545 wrapped_gitaly_errors do 546 gitaly_submodule_url_for(ref, path) 547 end 548 end 549 550 # Returns path to url mappings for submodules 551 # 552 # Ex. 553 # @repository.submodule_urls_for('master') 554 # # => { 'rack' => 'git@localhost:rack.git' } 555 # 556 def submodule_urls_for(ref) 557 wrapped_gitaly_errors do 558 gitaly_submodule_urls_for(ref) 559 end 560 end 561 562 # Return total commits count accessible from passed ref 563 def commit_count(ref) 564 wrapped_gitaly_errors do 565 gitaly_commit_client.commit_count(ref) 566 end 567 end 568 569 # Return total diverging commits count 570 def diverging_commit_count(from, to, max_count: 0) 571 wrapped_gitaly_errors do 572 gitaly_commit_client.diverging_commit_count(from, to, max_count: max_count) 573 end 574 end 575 576 # Mimic the `git clean` command and recursively delete untracked files. 577 # Valid keys that can be passed in the +options+ hash are: 578 # 579 # :d - Remove untracked directories 580 # :f - Remove untracked directories that are managed by a different 581 # repository 582 # :x - Remove ignored files 583 # 584 # The value in +options+ must evaluate to true for an option to take 585 # effect. 586 # 587 # Examples: 588 # 589 # repo.clean(d: true, f: true) # Enable the -d and -f options 590 # 591 # repo.clean(d: false, x: true) # -x is enabled, -d is not 592 def clean(options = {}) 593 strategies = [:remove_untracked] 594 strategies.push(:force) if options[:f] 595 strategies.push(:remove_ignored) if options[:x] 596 597 # TODO: implement this method 598 end 599 600 def add_branch(branch_name, user:, target:) 601 wrapped_gitaly_errors do 602 gitaly_operation_client.user_create_branch(branch_name, user, target) 603 end 604 end 605 606 def add_tag(tag_name, user:, target:, message: nil) 607 wrapped_gitaly_errors do 608 gitaly_operation_client.add_tag(tag_name, user, target, message) 609 end 610 end 611 612 def update_branch(branch_name, user:, newrev:, oldrev:) 613 wrapped_gitaly_errors do 614 gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev) 615 end 616 end 617 618 def rm_branch(branch_name, user:) 619 wrapped_gitaly_errors do 620 gitaly_operation_client.user_delete_branch(branch_name, user) 621 end 622 end 623 624 def rm_tag(tag_name, user:) 625 wrapped_gitaly_errors do 626 gitaly_operation_client.rm_tag(tag_name, user) 627 end 628 end 629 630 def merge_to_ref(user, **kwargs) 631 wrapped_gitaly_errors do 632 gitaly_operation_client.user_merge_to_ref(user, **kwargs) 633 end 634 end 635 636 def merge(user, source_sha, target_branch, message, &block) 637 wrapped_gitaly_errors do 638 gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block) 639 end 640 end 641 642 def ff_merge(user, source_sha, target_branch) 643 wrapped_gitaly_errors do 644 gitaly_operation_client.user_ff_branch(user, source_sha, target_branch) 645 end 646 end 647 648 def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:, dry_run: false) 649 args = { 650 user: user, 651 commit: commit, 652 branch_name: branch_name, 653 message: message, 654 start_branch_name: start_branch_name, 655 start_repository: start_repository, 656 dry_run: dry_run 657 } 658 659 wrapped_gitaly_errors do 660 gitaly_operation_client.user_revert(**args) 661 end 662 end 663 664 def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:, dry_run: false) 665 args = { 666 user: user, 667 commit: commit, 668 branch_name: branch_name, 669 message: message, 670 start_branch_name: start_branch_name, 671 start_repository: start_repository, 672 dry_run: dry_run 673 } 674 675 wrapped_gitaly_errors do 676 gitaly_operation_client.user_cherry_pick(**args) 677 end 678 end 679 680 def update_submodule(user:, submodule:, commit_sha:, message:, branch:) 681 args = { 682 user: user, 683 submodule: submodule, 684 commit_sha: commit_sha, 685 branch: branch, 686 message: message 687 } 688 689 wrapped_gitaly_errors do 690 gitaly_operation_client.user_update_submodule(**args) 691 end 692 end 693 694 # Delete the specified branch from the repository 695 # Note: No Git hooks are executed for this action 696 def delete_branch(branch_name) 697 write_ref(branch_name, Gitlab::Git::BLANK_SHA) 698 rescue CommandError => e 699 raise DeleteBranchError, e 700 end 701 702 def delete_refs(*ref_names) 703 wrapped_gitaly_errors do 704 gitaly_delete_refs(*ref_names) 705 end 706 end 707 708 # Create a new branch named **ref+ based on **stat_point+, HEAD by default 709 # Note: No Git hooks are executed for this action 710 # 711 # Examples: 712 # create_branch("feature") 713 # create_branch("other-feature", "master") 714 def create_branch(ref, start_point = "HEAD") 715 write_ref(ref, start_point) 716 end 717 718 def find_remote_root_ref(remote_url, authorization = nil) 719 return unless remote_url.present? 720 721 wrapped_gitaly_errors do 722 gitaly_remote_client.find_remote_root_ref(remote_url, authorization) 723 end 724 end 725 726 # Returns result like "git ls-files" , recursive and full file path 727 # 728 # Ex. 729 # repo.ls_files('master') 730 # 731 def ls_files(ref) 732 gitaly_commit_client.ls_files(ref) 733 end 734 735 def copy_gitattributes(ref) 736 wrapped_gitaly_errors do 737 gitaly_repository_client.apply_gitattributes(ref) 738 end 739 end 740 741 def info_attributes 742 return @info_attributes if @info_attributes 743 744 content = gitaly_repository_client.info_attributes 745 @info_attributes = AttributesParser.new(content) 746 end 747 748 # Returns the Git attributes for the given file path. 749 # 750 # See `Gitlab::Git::Attributes` for more information. 751 def attributes(path) 752 info_attributes.attributes(path) 753 end 754 755 def gitattribute(path, name) 756 attributes(path)[name] 757 end 758 759 # Returns parsed .gitattributes for a given ref 760 # 761 # This only parses the root .gitattributes file, 762 # it does not traverse subfolders to find additional .gitattributes files 763 # 764 # This method is around 30 times slower than `attributes`, which uses 765 # `$GIT_DIR/info/attributes`. Consider caching AttributesAtRefParser 766 # and reusing that for multiple calls instead of this method. 767 def attributes_at(ref) 768 AttributesAtRefParser.new(self, ref) 769 end 770 771 def languages(ref = nil) 772 wrapped_gitaly_errors do 773 gitaly_commit_client.languages(ref) 774 end 775 end 776 777 def license_short_name 778 wrapped_gitaly_errors do 779 gitaly_repository_client.license_short_name 780 end 781 end 782 783 def fetch_source_branch!(source_repository, source_branch, local_ref) 784 wrapped_gitaly_errors do 785 gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref) 786 end 787 end 788 789 def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) 790 CrossRepoComparer 791 .new(source_repository, self) 792 .compare(source_branch_name, target_branch_name, straight: straight) 793 end 794 795 def write_ref(ref_path, ref, old_ref: nil) 796 ref_path = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref_path}" unless ref_path.start_with?("refs/") || ref_path == "HEAD" 797 798 wrapped_gitaly_errors do 799 gitaly_repository_client.write_ref(ref_path, ref, old_ref) 800 end 801 end 802 803 def list_refs 804 wrapped_gitaly_errors do 805 gitaly_ref_client.list_refs 806 end 807 end 808 809 # Refactoring aid; allows us to copy code from app/models/repository.rb 810 def commit(ref = 'HEAD') 811 Gitlab::Git::Commit.find(self, ref) 812 end 813 814 def empty? 815 !has_visible_content? 816 end 817 818 # Fetch remote for repository 819 # 820 # remote - remote name 821 # url - URL of the remote to fetch. `remote` is not used in this case. 822 # refmap - if url is given, determines which references should get fetched where 823 # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication 824 # forced - should we use --force flag? 825 # no_tags - should we use --no-tags flag? 826 # prune - should we use --prune flag? 827 # check_tags_changed - should we ask gitaly to calculate whether any tags changed? 828 def fetch_remote(url, refmap: nil, ssh_auth: nil, forced: false, no_tags: false, prune: true, check_tags_changed: false, http_authorization_header: "") 829 wrapped_gitaly_errors do 830 gitaly_repository_client.fetch_remote( 831 url, 832 refmap: refmap, 833 ssh_auth: ssh_auth, 834 forced: forced, 835 no_tags: no_tags, 836 prune: prune, 837 check_tags_changed: check_tags_changed, 838 timeout: GITLAB_PROJECTS_TIMEOUT, 839 http_authorization_header: http_authorization_header 840 ) 841 end 842 end 843 844 def import_repository(url) 845 raise ArgumentError, "don't use disk paths with import_repository: #{url.inspect}" if url.start_with?('.', '/') 846 847 wrapped_gitaly_errors do 848 gitaly_repository_client.import_repository(url) 849 end 850 end 851 852 def blob_at(sha, path, limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) 853 Gitlab::Git::Blob.find(self, sha, path, limit: limit) unless Gitlab::Git.blank_ref?(sha) 854 end 855 856 # Items should be of format [[commit_id, path], [commit_id1, path1]] 857 def batch_blobs(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) 858 Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit) 859 end 860 861 def fsck 862 msg, status = gitaly_repository_client.fsck 863 864 raise GitError, "Could not fsck repository: #{msg}" unless status == 0 865 end 866 867 def create_from_bundle(bundle_path) 868 # It's important to check that the linked-to file is actually a valid 869 # .bundle file as it is passed to `git clone`, which may otherwise 870 # interpret it as a pointer to another repository 871 ::Gitlab::Git::BundleFile.check!(bundle_path) 872 873 gitaly_repository_client.create_from_bundle(bundle_path) 874 end 875 876 def create_from_snapshot(url, auth) 877 gitaly_repository_client.create_from_snapshot(url, auth) 878 end 879 880 def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, push_options: [], &block) 881 wrapped_gitaly_errors do 882 gitaly_operation_client.rebase( 883 user, 884 rebase_id, 885 branch: branch, 886 branch_sha: branch_sha, 887 remote_repository: remote_repository, 888 remote_branch: remote_branch, 889 push_options: push_options, 890 &block 891 ) 892 end 893 end 894 895 def squash(user, start_sha:, end_sha:, author:, message:) 896 wrapped_gitaly_errors do 897 gitaly_operation_client.user_squash(user, start_sha, end_sha, author, message) 898 end 899 end 900 901 def bundle_to_disk(save_path) 902 wrapped_gitaly_errors do 903 gitaly_repository_client.create_bundle(save_path) 904 end 905 906 true 907 end 908 909 # rubocop:disable Metrics/ParameterLists 910 def multi_action( 911 user, branch_name:, message:, actions:, 912 author_email: nil, author_name: nil, 913 start_branch_name: nil, start_sha: nil, start_repository: self, 914 force: false) 915 916 wrapped_gitaly_errors do 917 gitaly_operation_client.user_commit_files(user, branch_name, 918 message, actions, author_email, author_name, 919 start_branch_name, start_repository, force, start_sha) 920 end 921 end 922 # rubocop:enable Metrics/ParameterLists 923 924 def set_full_path(full_path:) 925 return unless full_path.present? 926 927 # This guard avoids Gitaly log/error spam 928 raise NoRepository, 'repository does not exist' unless exists? 929 930 gitaly_repository_client.set_full_path(full_path) 931 end 932 933 def disconnect_alternates 934 wrapped_gitaly_errors do 935 gitaly_repository_client.disconnect_alternates 936 end 937 end 938 939 def gitaly_repository 940 Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository, @gl_project_path) 941 end 942 943 def gitaly_ref_client 944 @gitaly_ref_client ||= Gitlab::GitalyClient::RefService.new(self) 945 end 946 947 def gitaly_commit_client 948 @gitaly_commit_client ||= Gitlab::GitalyClient::CommitService.new(self) 949 end 950 951 def gitaly_repository_client 952 @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self) 953 end 954 955 def gitaly_operation_client 956 @gitaly_operation_client ||= Gitlab::GitalyClient::OperationService.new(self) 957 end 958 959 def gitaly_remote_client 960 @gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self) 961 end 962 963 def gitaly_blob_client 964 @gitaly_blob_client ||= Gitlab::GitalyClient::BlobService.new(self) 965 end 966 967 def gitaly_conflicts_client(our_commit_oid, their_commit_oid) 968 Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid) 969 end 970 971 def praefect_info_client 972 @praefect_info_client ||= Gitlab::GitalyClient::PraefectInfoService.new(self) 973 end 974 975 def clean_stale_repository_files 976 wrapped_gitaly_errors do 977 gitaly_repository_client.cleanup if exists? 978 end 979 rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup 980 Gitlab::AppLogger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}") 981 Gitlab::Metrics.counter( 982 :failed_repository_cleanup_total, 983 'Number of failed repository cleanup events' 984 ).increment 985 end 986 987 def branch_names_contains_sha(sha) 988 gitaly_ref_client.branch_names_contains_sha(sha) 989 end 990 991 def tag_names_contains_sha(sha) 992 gitaly_ref_client.tag_names_contains_sha(sha) 993 end 994 995 def search_files_by_content(query, ref, options = {}) 996 return [] if empty? || query.blank? 997 998 safe_query = Regexp.escape(query) 999 ref ||= root_ref 1000 1001 gitaly_repository_client.search_files_by_content(ref, safe_query, options) 1002 end 1003 1004 def can_be_merged?(source_sha, target_branch) 1005 if target_sha = find_branch(target_branch)&.target 1006 !gitaly_conflicts_client(source_sha, target_sha).conflicts? 1007 else 1008 false 1009 end 1010 end 1011 1012 def search_files_by_name(query, ref) 1013 safe_query = Regexp.escape(query.sub(%r{^/*}, "")) 1014 ref ||= root_ref 1015 1016 return [] if empty? || safe_query.blank? 1017 1018 gitaly_repository_client.search_files_by_name(ref, safe_query) 1019 end 1020 1021 def search_files_by_regexp(filter, ref = 'HEAD') 1022 gitaly_repository_client.search_files_by_regexp(ref, filter) 1023 end 1024 1025 def find_commits_by_message(query, ref, path, limit, offset) 1026 wrapped_gitaly_errors do 1027 gitaly_commit_client 1028 .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset) 1029 .map { |c| commit(c) } 1030 end 1031 end 1032 1033 def list_last_commits_for_tree(sha, path, offset: 0, limit: 25, literal_pathspec: false) 1034 wrapped_gitaly_errors do 1035 gitaly_commit_client.list_last_commits_for_tree(sha, path, offset: offset, limit: limit, literal_pathspec: literal_pathspec) 1036 end 1037 end 1038 1039 def list_commits_by_ref_name(refs) 1040 wrapped_gitaly_errors do 1041 gitaly_commit_client.list_commits_by_ref_name(refs) 1042 end 1043 end 1044 1045 def last_commit_for_path(sha, path, literal_pathspec: false) 1046 wrapped_gitaly_errors do 1047 gitaly_commit_client.last_commit_for_path(sha, path, literal_pathspec: literal_pathspec) 1048 end 1049 end 1050 1051 def checksum 1052 # The exists? RPC is much cheaper, so we perform this request first 1053 raise NoRepository, "Repository does not exists" unless exists? 1054 1055 gitaly_repository_client.calculate_checksum 1056 rescue GRPC::NotFound 1057 raise NoRepository # Guard against data races. 1058 end 1059 1060 def replicas 1061 wrapped_gitaly_errors do 1062 praefect_info_client.replicas 1063 end 1064 end 1065 1066 private 1067 1068 def empty_diff_stats 1069 Gitlab::Git::DiffStatsCollection.new([]) 1070 end 1071 1072 def uncached_has_local_branches? 1073 wrapped_gitaly_errors do 1074 gitaly_repository_client.has_local_branches? 1075 end 1076 end 1077 1078 def gitaly_merged_branch_names(branch_names, root_sha) 1079 qualified_branch_names = branch_names.map { |b| "refs/heads/#{b}" } 1080 1081 gitaly_ref_client.merged_branches(qualified_branch_names) 1082 .reject { |b| b.target == root_sha } 1083 .map(&:name) 1084 end 1085 1086 def process_count_commits_options(options) 1087 if options[:from] || options[:to] 1088 ref = 1089 if options[:left_right] # Compare with merge-base for left-right 1090 "#{options[:from]}...#{options[:to]}" 1091 else 1092 "#{options[:from]}..#{options[:to]}" 1093 end 1094 1095 options.merge(ref: ref) 1096 1097 elsif options[:ref] && options[:left_right] 1098 from, to = options[:ref].match(/\A([^\.]*)\.{2,3}([^\.]*)\z/)[1..2] 1099 1100 options.merge(from: from, to: to) 1101 else 1102 options 1103 end 1104 end 1105 1106 def gitaly_submodule_url_for(ref, path) 1107 # We don't care about the contents so 1 byte is enough. Can't request 0 bytes, 0 means unlimited. 1108 commit_object = gitaly_commit_client.tree_entry(ref, path, 1) 1109 1110 return unless commit_object && commit_object.type == :COMMIT 1111 1112 urls = gitaly_submodule_urls_for(ref) 1113 urls && urls[path] 1114 end 1115 1116 def gitaly_submodule_urls_for(ref) 1117 gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) 1118 return unless gitmodules 1119 1120 submodules = GitmodulesParser.new(gitmodules.data).parse 1121 submodules.transform_values { |submodule| submodule['url'] } 1122 end 1123 1124 # Returns true if the given ref name exists 1125 # 1126 # Ref names must start with `refs/`. 1127 def gitaly_ref_exists?(ref_name) 1128 gitaly_ref_client.ref_exists?(ref_name) 1129 end 1130 1131 def gitaly_copy_gitattributes(revision) 1132 gitaly_repository_client.apply_gitattributes(revision) 1133 end 1134 1135 def gitaly_delete_refs(*ref_names) 1136 gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any? 1137 end 1138 end 1139 end 1140end 1141