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