repository.rb 33.2 KB
Newer Older
1 2
require 'securerandom'

3
class Repository
4 5
  include Gitlab::ShellAdapter

6
  attr_accessor :path_with_namespace, :project
7

8
  CommitError = Class.new(StandardError)
9

10 11 12 13 14 15 16 17 18 19 20
  # Methods that cache data from the Git repository.
  #
  # Each entry in this Array should have a corresponding method with the exact
  # same name. The cache key used by those methods must also match method's
  # name.
  #
  # For example, for entry `:readme` there's a method called `readme` which
  # stores its data in the `readme` cache key.
  CACHED_METHODS = %i(size commit_count readme version contribution_guide
                      changelog license_blob license_key gitignore koding_yml
                      gitlab_ci_yml branch_names tag_names branch_count
D
Douwe Maan 已提交
21
                      tag_count avatar exists? empty? root_ref).freeze
22 23 24 25 26 27 28 29 30 31 32 33 34 35

  # Certain method caches should be refreshed when certain types of files are
  # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
  # the corresponding methods to call for refreshing caches.
  METHOD_CACHES_FOR_FILE_TYPES = {
    readme: :readme,
    changelog: :changelog,
    license: %i(license_blob license_key),
    contributing: :contribution_guide,
    version: :version,
    gitignore: :gitignore,
    koding: :koding_yml,
    gitlab_ci: :gitlab_ci_yml,
    avatar: :avatar
D
Douwe Maan 已提交
36
  }.freeze
37 38 39 40 41 42 43

  # Wraps around the given method and caches its output in Redis and an instance
  # variable.
  #
  # This only works for methods that do not take any arguments.
  def self.cache_method(name, fallback: nil)
    original = :"_uncached_#{name}"
44

45
    alias_method(original, name)
46

47 48
    define_method(name) do
      cache_method_output(name, fallback: fallback) { __send__(original) }
49
    end
50
  end
51 52 53 54 55

  def self.storages
    Gitlab.config.repositories.storages
  end

56
  def initialize(path_with_namespace, project)
57
    @path_with_namespace = path_with_namespace
58
    @project = project
59
  end
60

61 62
  def raw_repository
    return nil unless path_with_namespace
63

64
    @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
65 66
  end

67
  # Return absolute path to repository
68
  def path_to_repo
69
    @path_to_repo ||= File.expand_path(
70
      File.join(@project.repository_storage_path, path_with_namespace + ".git")
71
    )
72 73
  end

74 75 76 77 78 79 80 81 82 83
  #
  # 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.
  #
  # This method return true if repository contains some content visible in project page.
  #
  def has_visible_content?
84
    branch_count > 0
85 86
  end

L
Lin Jen-Shin 已提交
87
  def commit(ref = 'HEAD')
88
    return nil unless exists?
89

90 91 92 93 94 95
    commit =
      if ref.is_a?(Gitlab::Git::Commit)
        ref
      else
        Gitlab::Git::Commit.find(raw_repository, ref)
      end
96

97
    commit = ::Commit.new(commit, @project) if commit
98
    commit
99
  rescue Rugged::OdbError, Rugged::TreeError
100
    nil
101 102
  end

103
  def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
104
    options = {
105 106 107 108 109
      repo: raw_repository,
      ref: ref,
      path: path,
      limit: limit,
      offset: offset,
110 111
      after: after,
      before: before,
112 113
      # --follow doesn't play well with --skip. See:
      # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
114 115
      follow: false,
      skip_merges: skip_merges
116 117 118
    }

    commits = Gitlab::Git::Commit.where(options)
119
    commits = Commit.decorate(commits, @project) if commits.present?
120 121 122
    commits
  end

123 124
  def commits_between(from, to)
    commits = Gitlab::Git::Commit.between(raw_repository, from, to)
125
    commits = Commit.decorate(commits, @project) if commits.present?
126 127 128
    commits
  end

129
  def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
130 131 132 133
    unless exists? && has_visible_content? && query.present?
      return []
    end

134 135
    ref ||= root_ref

136 137 138 139
    args = %W(
      #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset}
      --max-count #{limit} --grep=#{query} --regexp-ignore-case
    )
140
    args = args.concat(%W(-- #{path})) if path.present?
141

142 143
    git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
    git_log_results.map { |c| commit(c.chomp) }.compact
144 145
  end

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
  def find_branch(name, fresh_repo: true)
    # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
    # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
    # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
    # may cause the branch to "disappear" erroneously or have the wrong SHA.
    #
    # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
    raw_repo =
      if fresh_repo
        Gitlab::Git::Repository.new(path_to_repo)
      else
        raw_repository
      end

    raw_repo.find_branch(name)
161 162 163
  end

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

167 168
  def add_branch(user, branch_name, ref)
    newrev = commit(ref).try(:sha)
169

170
    return false unless newrev
171

172
    GitOperationService.new(user, self).add_branch(branch_name, newrev)
173

174
    after_create_branch
175
    find_branch(branch_name)
176 177
  end

178
  def add_tag(user, tag_name, target, message = nil)
179
    newrev = commit(target).try(:id)
180 181
    options = { message: message, tagger: user_to_committer(user) } if message

182 183 184
    return false unless newrev

    GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
185

186
    find_tag(tag_name)
187 188
  end

189
  def rm_branch(user, branch_name)
190
    before_remove_branch
191 192
    branch = find_branch(branch_name)

193
    GitOperationService.new(user, self).rm_branch(branch)
194

195
    after_remove_branch
196
    true
197 198
  end

L
Lin Jen-Shin 已提交
199
  def rm_tag(user, tag_name)
Y
Yorick Peterse 已提交
200
    before_remove_tag
L
Lin Jen-Shin 已提交
201
    tag = find_tag(tag_name)
202

L
Lin Jen-Shin 已提交
203 204 205 206
    GitOperationService.new(user, self).rm_tag(tag)

    after_remove_tag
    true
207 208
  end

209 210 211 212
  def ref_names
    branch_names + tag_names
  end

213 214 215 216
  def branch_exists?(branch_name)
    branch_names.include?(branch_name)
  end

217 218
  def ref_exists?(ref)
    rugged.references.exist?(ref)
219 220
  rescue Rugged::ReferenceError
    false
221 222
  end

D
Douwe Maan 已提交
223 224 225 226
  # Makes sure a commit is kept around when Git garbage collection runs.
  # Git GC will delete commits from the repository that are no longer in any
  # branches or tags, but we want to keep some of these commits around, for
  # example if they have comments or CI builds.
227 228 229 230 231
  def keep_around(sha)
    return unless sha && commit(sha)

    return if kept_around?(sha)

232 233 234 235 236
    # This will still fail if the file is corrupted (e.g. 0 bytes)
    begin
      rugged.references.create(keep_around_ref_name(sha), sha, force: true)
    rescue Rugged::ReferenceError => ex
      Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
237 238 239
    rescue Rugged::OSError => ex
      raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
      Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
240
    end
241 242 243
  end

  def kept_around?(sha)
244
    ref_exists?(keep_around_ref_name(sha))
245 246
  end

247
  def diverging_commit_counts(branch)
248
    root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
J
Jeff Stubler 已提交
249
    cache.fetch(:"diverging_commit_counts_#{branch.name}") do
250 251
      # Rugged seems to throw a `ReferenceError` when given branch_names rather
      # than SHA-1 hashes
252
      number_commits_behind = raw_repository.
253
        count_commits_between(branch.dereferenced_target.sha, root_ref_hash)
254 255

      number_commits_ahead = raw_repository.
256
        count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
257

258 259 260
      { behind: number_commits_behind, ahead: number_commits_ahead }
    end
  end
261

262 263 264
  def expire_tags_cache
    expire_method_caches(%i(tag_names tag_count))
    @tags = nil
265
  end
266

267 268 269
  def expire_branches_cache
    expire_method_caches(%i(branch_names branch_count))
    @local_branches = nil
270 271
  end

272 273
  def expire_statistics_caches
    expire_method_caches(%i(size commit_count))
274 275
  end

276 277
  def expire_all_method_caches
    expire_method_caches(CACHED_METHODS)
D
Douwe Maan 已提交
278 279
  end

280 281 282 283 284 285 286 287 288
  # Expires the caches of a specific set of methods
  def expire_method_caches(methods)
    methods.each do |key|
      cache.expire(key)

      ivar = cache_instance_variable_name(key)

      remove_instance_variable(ivar) if instance_variable_defined?(ivar)
    end
D
Douwe Maan 已提交
289 290
  end

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
  def expire_avatar_cache
    expire_method_caches(%i(avatar))
  end

  # Refreshes the method caches of this repository.
  #
  # types - An Array of file types (e.g. `:readme`) used to refresh extra
  #         caches.
  def refresh_method_caches(types)
    to_refresh = []

    types.each do |type|
      methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]

      to_refresh.concat(Array(methods)) if methods
306
    end
307

308
    expire_method_caches(to_refresh)
309

310
    to_refresh.each { |method| send(method) }
311
  end
312

313 314 315 316 317 318 319 320 321 322 323 324
  def expire_branch_cache(branch_name = nil)
    # When we push to the root branch we have to flush the cache for all other
    # branches as their statistics are based on the commits relative to the
    # root branch.
    if !branch_name || branch_name == root_ref
      branches.each do |branch|
        cache.expire(:"diverging_commit_counts_#{branch.name}")
      end
    # In case a commit is pushed to a non-root branch we only have to flush the
    # cache for said branch.
    else
      cache.expire(:"diverging_commit_counts_#{branch_name}")
325
    end
D
Dmitriy Zaporozhets 已提交
326 327
  end

328
  def expire_root_ref_cache
329
    expire_method_caches(%i(root_ref))
330 331
  end

332 333
  # Expires the cache(s) used to determine if a repository is empty or not.
  def expire_emptiness_caches
334
    return unless empty?
335

336
    expire_method_caches(%i(empty?))
337 338
  end

339 340 341 342
  def lookup_cache
    @lookup_cache ||= {}
  end

343
  def expire_exists_cache
344
    expire_method_caches(%i(exists?))
345 346
  end

347 348 349 350 351 352 353
  # expire cache that doesn't depend on repository data (when expiring)
  def expire_content_cache
    expire_tags_cache
    expire_branches_cache
    expire_root_ref_cache
    expire_emptiness_caches
    expire_exists_cache
354
    expire_statistics_caches
355 356
  end

357 358 359
  # Runs code after a repository has been created.
  def after_create
    expire_exists_cache
360 361
    expire_root_ref_cache
    expire_emptiness_caches
Y
Yorick Peterse 已提交
362 363

    repository_event(:create_repository)
364 365
  end

366 367
  # Runs code just before a repository is deleted.
  def before_delete
368
    expire_exists_cache
369 370
    expire_all_method_caches
    expire_branch_cache if exists?
371
    expire_content_cache
Y
Yorick Peterse 已提交
372 373

    repository_event(:remove_repository)
374 375 376 377 378 379 380
  end

  # Runs code just before the HEAD of a repository is changed.
  def before_change_head
    # Cached divergent commit counts are based on repository head
    expire_branch_cache
    expire_root_ref_cache
Y
Yorick Peterse 已提交
381 382

    repository_event(:change_default_branch)
383 384
  end

Y
Yorick Peterse 已提交
385 386
  # Runs code before pushing (= creating or removing) a tag.
  def before_push_tag
387 388
    expire_statistics_caches
    expire_emptiness_caches
389
    expire_tags_cache
Y
Yorick Peterse 已提交
390 391

    repository_event(:push_tag)
Y
Yorick Peterse 已提交
392 393 394 395 396
  end

  # Runs code before removing a tag.
  def before_remove_tag
    expire_tags_cache
397
    expire_statistics_caches
Y
Yorick Peterse 已提交
398 399

    repository_event(:remove_tag)
400 401
  end

L
Lin Jen-Shin 已提交
402 403 404 405 406
  # Runs code after removing a tag.
  def after_remove_tag
    expire_tags_cache
  end

407
  def before_import
408
    expire_content_cache
409 410
  end

411 412 413 414 415
  # Runs code after the HEAD of a repository is changed.
  def after_change_head
    expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
  end

416 417
  # Runs code after a repository has been forked/imported.
  def after_import
418
    expire_content_cache
419 420
    expire_tags_cache
    expire_branches_cache
421 422 423
  end

  # Runs code after a new commit has been pushed.
424 425 426
  def after_push_commit(branch_name)
    expire_statistics_caches
    expire_branch_cache(branch_name)
Y
Yorick Peterse 已提交
427 428

    repository_event(:push_commit, branch: branch_name)
429 430 431 432
  end

  # Runs code after a new branch has been created.
  def after_create_branch
433
    expire_branches_cache
Y
Yorick Peterse 已提交
434 435

    repository_event(:push_branch)
436 437
  end

438 439 440
  # Runs code before removing an existing branch.
  def before_remove_branch
    expire_branches_cache
Y
Yorick Peterse 已提交
441 442

    repository_event(:remove_branch)
443 444
  end

445 446
  # Runs code after an existing branch has been removed.
  def after_remove_branch
447
    expire_branches_cache
448 449
  end

450
  def method_missing(m, *args, &block)
451 452 453 454 455 456
    if m == :lookup && !block_given?
      lookup_cache[m] ||= {}
      lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block)
    else
      raw_repository.send(m, *args, &block)
    end
457 458
  end

459 460
  def respond_to_missing?(method, include_private = false)
    raw_repository.respond_to?(method, include_private) || super
461
  end
D
Dmitriy Zaporozhets 已提交
462 463

  def blob_at(sha, path)
464
    unless Gitlab::Git.blank_ref?(sha)
465
      Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
466
    end
D
Douwe Maan 已提交
467 468
  rescue Gitlab::Git::Repository::NoRepository
    nil
D
Dmitriy Zaporozhets 已提交
469
  end
470

471 472 473 474
  def blob_by_oid(oid)
    Gitlab::Git::Blob.raw(self, oid)
  end

475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
  def root_ref
    if raw_repository
      raw_repository.root_ref
    else
      # When the repo does not exist we raise this error so no data is cached.
      raise Rugged::ReferenceError
    end
  end
  cache_method :root_ref

  def exists?
    refs_directory_exists?
  end
  cache_method :exists?

  def empty?
    raw_repository.empty?
  end
  cache_method :empty?

  # The size of this repository in megabytes.
  def size
    exists? ? raw_repository.size : 0.0
  end
  cache_method :size, fallback: 0.0

  def commit_count
    root_ref ? raw_repository.commit_count(root_ref) : 0
  end
  cache_method :commit_count, fallback: 0

  def branch_names
    branches.map(&:name)
  end
  cache_method :branch_names, fallback: []

  def tag_names
    raw_repository.tag_names
  end
  cache_method :tag_names, fallback: []

  def branch_count
    branches.size
  end
  cache_method :branch_count, fallback: 0

  def tag_count
    raw_repository.rugged.tags.count
  end
  cache_method :tag_count, fallback: 0

  def avatar
    if tree = file_on_head(:avatar)
      tree.path
    end
  end
  cache_method :avatar

533
  def readme
534 535 536
    if head = tree(:head)
      head.readme
    end
537
  end
538
  cache_method :readme
539

540
  def version
541
    file_on_head(:version)
542
  end
543
  cache_method :version
544

545
  def contribution_guide
546
    file_on_head(:contributing)
547
  end
548
  cache_method :contribution_guide
549 550

  def changelog
551
    file_on_head(:changelog)
552
  end
553
  cache_method :changelog
554

555
  def license_blob
556
    file_on_head(:license)
557
  end
558
  cache_method :license_blob
Z
Zeger-Jan van de Weg 已提交
559

560
  def license_key
561
    return unless exists?
562

563
    Licensee.license(path).try(:key)
564
  end
565
  cache_method :license_key
566

567
  def gitignore
568
    file_on_head(:gitignore)
569
  end
570
  cache_method :gitignore
571

572
  def koding_yml
573
    file_on_head(:koding)
574
  end
575
  cache_method :koding_yml
576

577
  def gitlab_ci_yml
578
    file_on_head(:gitlab_ci)
579
  end
580
  cache_method :gitlab_ci_yml
581

582
  def head_commit
583 584 585 586
    @head_commit ||= commit(self.root_ref)
  end

  def head_tree
587 588 589
    if head_commit
      @head_tree ||= Tree.new(self, head_commit.sha, nil)
    end
590 591
  end

592
  def tree(sha = :head, path = nil, recursive: false)
593
    if sha == :head
594 595
      return unless head_commit

596 597 598 599 600
      if path.nil?
        return head_tree
      else
        sha = head_commit.sha
      end
601 602
    end

603
    Tree.new(self, sha, path, recursive: recursive)
604
  end
D
Dmitriy Zaporozhets 已提交
605 606

  def blob_at_branch(branch_name, path)
D
Dmitriy Zaporozhets 已提交
607
    last_commit = commit(branch_name)
D
Dmitriy Zaporozhets 已提交
608

D
Dmitriy Zaporozhets 已提交
609 610 611 612 613
    if last_commit
      blob_at(last_commit.sha, path)
    else
      nil
    end
D
Dmitriy Zaporozhets 已提交
614
  end
D
Dmitriy Zaporozhets 已提交
615 616 617 618 619 620 621 622

  # Returns url for submodule
  #
  # Ex.
  #   @repository.submodule_url_for('master', 'rack')
  #   # => git@localhost:rack.git
  #
  def submodule_url_for(ref, path)
D
Dmitriy Zaporozhets 已提交
623
    if submodules(ref).any?
D
Dmitriy Zaporozhets 已提交
624 625 626 627 628 629 630
      submodule = submodules(ref)[path]

      if submodule
        submodule['url']
      end
    end
  end
631 632

  def last_commit_for_path(sha, path)
H
Hiroyuki Sato 已提交
633
    sha = last_commit_id_for_path(sha, path)
634
    commit(sha)
635
  end
636

H
Hiroyuki Sato 已提交
637 638
  def last_commit_id_for_path(sha, path)
    key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
H
Hiroyuki Sato 已提交
639

H
Hiroyuki Sato 已提交
640
    cache.fetch(key) do
H
Hiroyuki Sato 已提交
641 642
      args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
      Gitlab::Popen.popen(args, path_to_repo).first.strip
H
Hiroyuki Sato 已提交
643 644 645
    end
  end

646
  def next_branch(name, opts = {})
P
P.S.V.R 已提交
647 648 649
    branch_ids = self.branch_names.map do |n|
      next 1 if n == name
      result = n.match(/\A#{name}-([0-9]+)\z/)
650 651 652
      result[1].to_i if result
    end.compact

P
P.S.V.R 已提交
653
    highest_branch_id = branch_ids.max || 0
654

P
P.S.V.R 已提交
655 656 657
    return name if opts[:mild] && 0 == highest_branch_id

    "#{name}-#{highest_branch_id + 1}"
658 659
  end

660
  # Remove archives older than 2 hours
661 662
  def branches_sorted_by(value)
    case value
663 664
    when 'name'
      branches.sort_by(&:name)
665
    when 'updated_desc'
666
      branches.sort do |a, b|
667
        commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
668
      end
669
    when 'updated_asc'
670
      branches.sort do |a, b|
671
        commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
672 673 674 675 676
      end
    else
      branches
    end
  end
677

678 679 680
  def tags_sorted_by(value)
    case value
    when 'name'
681
      VersionSorter.rsort(tags) { |tag| tag.name }
682 683 684 685 686 687 688 689 690
    when 'updated_desc'
      tags_sorted_by_committed_date.reverse
    when 'updated_asc'
      tags_sorted_by_committed_date
    else
      tags
    end
  end

691
  def contributors
692
    commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
693

D
Dmitriy Zaporozhets 已提交
694
    commits.group_by(&:author_email).map do |email, commits|
695 696
      contributor = Gitlab::Contributor.new
      contributor.email = email
697

D
Dmitriy Zaporozhets 已提交
698
      commits.each do |commit|
699
        if contributor.name.blank?
D
Dmitriy Zaporozhets 已提交
700
          contributor.name = commit.author_name
701 702
        end

703
        contributor.commits += 1
704 705
      end

706 707
      contributor
    end
708
  end
D
Dmitriy Zaporozhets 已提交
709

710 711
  def ref_name_for_sha(ref_path, sha)
    args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
712 713 714 715 716 717

    # Not found -> ["", 0]
    # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
    Gitlab::Popen.popen(args, path_to_repo).first.split.last
  end

718 719
  def refs_contains_sha(ref_type, sha)
    args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
720 721 722 723 724 725 726 727 728 729 730 731 732 733
    names = Gitlab::Popen.popen(args, path_to_repo).first

    if names.respond_to?(:split)
      names = names.split("\n").map(&:strip)

      names.each do |name|
        name.slice! '* '
      end

      names
    else
      []
    end
  end
H
Hannes Rosenögger 已提交
734

735 736 737
  def branch_names_contains(sha)
    refs_contains_sha('branch', sha)
  end
H
Hannes Rosenögger 已提交
738

739 740
  def tag_names_contains(sha)
    refs_contains_sha('tag', sha)
H
Hannes Rosenögger 已提交
741
  end
742

743
  def local_branches
744
    @local_branches ||= raw_repository.local_branches
745 746
  end

747 748
  alias_method :branches, :local_branches

749 750 751 752
  def tags
    @tags ||= raw_repository.tags
  end

753
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
754
  def commit_dir(
755 756
    user, path,
    message:, branch_name:,
757
    author_email: nil, author_name: nil,
758
    start_branch_name: nil, start_project: project)
759 760
    check_tree_entry_for_dir(branch_name, path)

761 762 763
    if start_branch_name
      start_project.repository.
        check_tree_entry_for_dir(start_branch_name, path)
L
Lin Jen-Shin 已提交
764 765 766 767
    end

    commit_file(
      user,
768
      "#{path}/.gitkeep",
L
Lin Jen-Shin 已提交
769
      '',
770 771 772
      message: message,
      branch_name: branch_name,
      update: false,
L
Lin Jen-Shin 已提交
773 774
      author_email: author_email,
      author_name: author_name,
775 776
      start_branch_name: start_branch_name,
      start_project: start_project)
S
Stan Hu 已提交
777
  end
778
  # rubocop:enable Metrics/ParameterLists
779

L
Lin Jen-Shin 已提交
780 781
  # rubocop:disable Metrics/ParameterLists
  def commit_file(
782 783
    user, path, content,
    message:, branch_name:, update: true,
784
    author_email: nil, author_name: nil,
785
    start_branch_name: nil, start_project: project)
786 787 788 789 790 791 792
    unless update
      error_message = "Filename already exists; update not allowed"

      if tree_entry_at(branch_name, path)
        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
      end

793 794
      if start_branch_name &&
          start_project.repository.tree_entry_at(start_branch_name, path)
795
        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
796 797 798
      end
    end

799 800 801
    multi_action(
      user: user,
      message: message,
802
      branch_name: branch_name,
803 804
      author_email: author_email,
      author_name: author_name,
805 806
      start_branch_name: start_branch_name,
      start_project: start_project,
L
Lin Jen-Shin 已提交
807 808 809
      actions: [{ action: :create,
                  file_path: path,
                  content: content }])
810
  end
L
Lin Jen-Shin 已提交
811
  # rubocop:enable Metrics/ParameterLists
812

L
Lin Jen-Shin 已提交
813 814 815
  # rubocop:disable Metrics/ParameterLists
  def update_file(
    user, path, content,
816
    message:, branch_name:, previous_path:,
817
    author_email: nil, author_name: nil,
818
    start_branch_name: nil, start_project: project)
819 820 821 822 823 824 825 826 827
    action = if previous_path && previous_path != path
               :move
             else
               :update
             end

    multi_action(
      user: user,
      message: message,
828
      branch_name: branch_name,
829 830
      author_email: author_email,
      author_name: author_name,
831 832
      start_branch_name: start_branch_name,
      start_project: start_project,
L
Lin Jen-Shin 已提交
833 834 835 836
      actions: [{ action: action,
                  file_path: path,
                  content: content,
                  previous_path: previous_path }])
837
  end
L
Lin Jen-Shin 已提交
838
  # rubocop:enable Metrics/ParameterLists
839

840
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
841
  def remove_file(
842 843
    user, path,
    message:, branch_name:,
844
    author_email: nil, author_name: nil,
845
    start_branch_name: nil, start_project: project)
846 847 848
    multi_action(
      user: user,
      message: message,
849
      branch_name: branch_name,
850 851
      author_email: author_email,
      author_name: author_name,
852 853
      start_branch_name: start_branch_name,
      start_project: start_project,
L
Lin Jen-Shin 已提交
854 855
      actions: [{ action: :delete,
                  file_path: path }])
856
  end
857
  # rubocop:enable Metrics/ParameterLists
858

859
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
860
  def multi_action(
861
    user:, branch_name:, message:, actions:,
862
    author_email: nil, author_name: nil,
863
    start_branch_name: nil, start_project: project)
864
    GitOperationService.new(user, self).with_branch(
865
      branch_name,
866 867
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
M
Marc Siegfriedt 已提交
868
      index = rugged.index
869

870 871 872
      parents = if start_commit
                  index.read_tree(start_commit.raw_commit.tree)
                  [start_commit.sha]
873 874 875
                else
                  []
                end
M
Marc Siegfriedt 已提交
876

877 878
      actions.each do |act|
        git_action(index, act)
M
Marc Siegfriedt 已提交
879 880 881 882 883 884 885 886 887 888 889 890
      end

      options = {
        tree: index.write_tree(rugged),
        message: message,
        parents: parents
      }
      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))

      Rugged::Commit.create(rugged, options)
    end
  end
891
  # rubocop:enable Metrics/ParameterLists
M
Marc Siegfriedt 已提交
892

893 894
  def get_committer_and_author(user, email: nil, name: nil)
    committer = user_to_committer(user)
D
Douwe Maan 已提交
895
    author = Gitlab::Git.committer_hash(email: email, name: name) || committer
896

897
    {
898 899
      author: author,
      committer: committer
900 901 902
    }
  end

903
  def user_to_committer(user)
904
    Gitlab::Git.committer_hash(email: user.email, name: user.name)
905 906
  end

907 908 909 910 911 912 913 914 915 916 917
  def can_be_merged?(source_sha, target_branch)
    our_commit = rugged.branches[target_branch].target
    their_commit = rugged.lookup(source_sha)

    if our_commit && their_commit
      !rugged.merge_commits(our_commit, their_commit).conflicts?
    else
      false
    end
  end

S
Sean McGivern 已提交
918
  def merge(user, source, merge_request, options = {})
919
    GitOperationService.new(user, self).with_branch(
920 921
      merge_request.target_branch) do |start_commit|
      our_commit = start_commit.sha
S
Sean McGivern 已提交
922
      their_commit = source
923

924 925
      raise 'Invalid merge target' unless our_commit
      raise 'Invalid merge source' unless their_commit
926

927 928
      merge_index = rugged.merge_commits(our_commit, their_commit)
      break if merge_index.conflicts?
929

930 931 932 933
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
      )
934

935 936 937
      commit_id = Rugged::Commit.create(rugged, actual_options)
      merge_request.update(in_progress_merge_commit_sha: commit_id)
      commit_id
938
    end
939 940
  rescue Repository::CommitError # when merge_index.conflicts?
    false
941 942
  end

943
  def revert(
944
    user, commit, branch_name, revert_tree_id = nil,
945
    start_branch_name: nil, start_project: project)
946
    revert_tree_id ||= check_revert_content(commit, branch_name)
947

948
    return false unless revert_tree_id
949

950
    GitOperationService.new(user, self).with_branch(
951
      branch_name,
952 953
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
954

955
      committer = user_to_committer(user)
956

L
Lin Jen-Shin 已提交
957
      Rugged::Commit.create(rugged,
958
        message: commit.revert_message(user),
959 960
        author: committer,
        committer: committer,
961
        tree: revert_tree_id,
962
        parents: [start_commit.sha])
963
    end
964 965
  end

966
  def cherry_pick(
967
    user, commit, branch_name, cherry_pick_tree_id = nil,
968
    start_branch_name: nil, start_project: project)
969
    cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
P
P.S.V.R 已提交
970 971 972

    return false unless cherry_pick_tree_id

973
    GitOperationService.new(user, self).with_branch(
974
      branch_name,
975 976
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
977

P
P.S.V.R 已提交
978
      committer = user_to_committer(user)
979

L
Lin Jen-Shin 已提交
980
      Rugged::Commit.create(rugged,
P
P.S.V.R 已提交
981 982 983 984 985 986 987 988
        message: commit.message,
        author: {
          email: commit.author_email,
          name: commit.author_name,
          time: commit.authored_date
        },
        committer: committer,
        tree: cherry_pick_tree_id,
989
        parents: [start_commit.sha])
P
P.S.V.R 已提交
990 991 992
    end
  end

993 994
  def resolve_conflicts(user, branch_name, params)
    GitOperationService.new(user, self).with_branch(branch_name) do
995 996 997 998 999 1000
      committer = user_to_committer(user)

      Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
    end
  end

1001 1002 1003 1004
  def check_revert_content(target_commit, branch_name)
    source_sha = commit(branch_name).sha
    args       = [target_commit.sha, source_sha]
    args << { mainline: 1 } if target_commit.merge_commit?
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014

    revert_index = rugged.revert_commit(*args)
    return false if revert_index.conflicts?

    tree_id = revert_index.write_tree(rugged)
    return false unless diff_exists?(source_sha, tree_id)

    tree_id
  end

1015 1016 1017 1018
  def check_cherry_pick_content(target_commit, branch_name)
    source_sha = commit(branch_name).sha
    args       = [target_commit.sha, source_sha]
    args << 1 if target_commit.merge_commit?
P
P.S.V.R 已提交
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028

    cherry_pick_index = rugged.cherrypick_commit(*args)
    return false if cherry_pick_index.conflicts?

    tree_id = cherry_pick_index.write_tree(rugged)
    return false unless diff_exists?(source_sha, tree_id)

    tree_id
  end

1029 1030
  def diff_exists?(sha1, sha2)
    rugged.diff(sha1, sha2).size > 0
1031 1032
  end

F
Florent (HP) 已提交
1033 1034 1035 1036 1037
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

    if branch_commit
1038 1039
      same_head = branch_commit.id == root_ref_commit.id
      !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
F
Florent (HP) 已提交
1040 1041 1042 1043 1044
    else
      nil
    end
  end

S
Stan Hu 已提交
1045
  def merge_base(first_commit_id, second_commit_id)
1046 1047
    first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
    second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
S
Stan Hu 已提交
1048
    rugged.merge_base(first_commit_id, second_commit_id)
D
Douwe Maan 已提交
1049 1050
  rescue Rugged::ReferenceError
    nil
S
Stan Hu 已提交
1051 1052
  end

1053 1054 1055 1056
  def is_ancestor?(ancestor_id, descendant_id)
    merge_base(ancestor_id, descendant_id) == ancestor_id
  end

V
Valery Sizov 已提交
1057 1058 1059 1060 1061 1062
  def empty_repo?
    !exists? || !has_visible_content?
  end

  def search_files_by_content(query, ref)
    return [] if empty_repo? || query.blank?
V
Valery Sizov 已提交
1063

1064
    offset = 2
1065
    args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
1066 1067 1068
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

V
Valery Sizov 已提交
1069 1070 1071 1072 1073 1074 1075
  def search_files_by_name(query, ref)
    return [] if empty_repo? || query.blank?

    args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
    Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
  end

1076
  def with_repo_branch_commit(start_repository, start_branch_name)
1077
    branch_name_or_sha =
1078 1079
      if start_repository == self
        start_branch_name
1080 1081
      else
        tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
1082

1083
        fetch_ref(
1084 1085
          start_repository.path_to_repo,
          "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
1086 1087 1088
          tmp_ref
        )

1089
        start_repository.commit(start_branch_name).sha
1090
      end
1091

1092
    yield(commit(branch_name_or_sha))
1093 1094

  ensure
1095
    rugged.references.delete(tmp_ref) if tmp_ref
1096 1097
  end

1098
  def fetch_ref(source_path, source_ref, target_ref)
1099
    args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
1100 1101 1102
    Gitlab::Popen.popen(args, path_to_repo)
  end

1103 1104 1105 1106
  def create_ref(ref, ref_path)
    fetch_ref(path_to_repo, ref, ref_path)
  end

1107 1108 1109 1110 1111
  def ls_files(ref)
    actual_ref = ref || root_ref
    raw_repository.ls_files(actual_ref)
  end

1112 1113 1114 1115
  def gitattribute(path, name)
    raw_repository.attributes(path)[name]
  end

1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
  def copy_gitattributes(ref)
    actual_ref = ref || root_ref
    begin
      raw_repository.copy_gitattributes(actual_ref)
      true
    rescue Gitlab::Git::Repository::InvalidRef
      false
    end
  end

1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138
  # Caches the supplied block both in a cache and in an instance variable.
  #
  # The cache key and instance variable are named the same way as the value of
  # the `key` argument.
  #
  # This method will return `nil` if the corresponding instance variable is also
  # set to `nil`. This ensures we don't keep yielding the block when it returns
  # `nil`.
  #
  # key - The name of the key to cache the data in.
  # fallback - A value to fall back to in the event of a Git error.
  def cache_method_output(key, fallback: nil, &block)
    ivar = cache_instance_variable_name(key)
1139

1140 1141 1142 1143 1144 1145 1146 1147 1148
    if instance_variable_defined?(ivar)
      instance_variable_get(ivar)
    else
      begin
        instance_variable_set(ivar, cache.fetch(key, &block))
      rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
        # if e.g. HEAD or the entire repository doesn't exist we want to
        # gracefully handle this and not cache anything.
        fallback
1149 1150 1151 1152
      end
    end
  end

1153 1154 1155
  def cache_instance_variable_name(key)
    :"@#{key.to_s.tr('?!', '')}"
  end
1156

1157 1158 1159 1160
  def file_on_head(type)
    if head = tree(:head)
      head.blobs.find do |file|
        Gitlab::FileDetector.type_of(file.name) == type
1161 1162
      end
    end
1163
  end
1164

D
Douwe Maan 已提交
1165 1166 1167 1168 1169 1170 1171 1172
  def route_map_for(sha)
    blob_data_at(sha, '.gitlab/route-map.yml')
  end

  def gitlab_ci_yml_for(sha)
    blob_data_at(sha, '.gitlab-ci.yml')
  end

1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
  protected

  def tree_entry_at(branch_name, path)
    branch_exists?(branch_name) &&
      # tree_entry is private
      raw_repository.send(:tree_entry, commit(branch_name), path)
  end

  def check_tree_entry_for_dir(branch_name, path)
    return unless branch_exists?(branch_name)

    entry = tree_entry_at(branch_name, path)

    return unless entry

    if entry[:type] == :blob
      raise Gitlab::Git::Repository::InvalidBlobName.new(
        "Directory already exists as a file")
    else
      raise Gitlab::Git::Repository::InvalidBlobName.new(
        "Directory already exists")
    end
  end

D
Douwe Maan 已提交
1197
  private
1198

D
Douwe Maan 已提交
1199 1200
  def blob_data_at(sha, path)
    blob = blob_at(sha, path)
1201
    return unless blob
1202

1203 1204 1205 1206
    blob.load_all_data!(self)
    blob.data
  end

1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232
  def git_action(index, action)
    path = normalize_path(action[:file_path])

    if action[:action] == :move
      previous_path = normalize_path(action[:previous_path])
    end

    case action[:action]
    when :create, :update, :move
      mode =
        case action[:action]
        when :update
          index.get(path)[:mode]
        when :move
          index.get(previous_path)[:mode]
        end
      mode ||= 0o100644

      index.remove(previous_path) if action[:action] == :move

      content = if action[:encoding] == 'base64'
                  Base64.decode64(action[:content])
                else
                  action[:content]
                end

1233 1234 1235 1236 1237 1238 1239 1240
      detect = CharlockHolmes::EncodingDetector.new.detect(content) if content

      unless detect && detect[:type] == :binary
        # When writing to the repo directly as we are doing here,
        # the `core.autocrlf` config isn't taken into account.
        content.gsub!("\r\n", "\n") if self.autocrlf
      end

1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258
      oid = rugged.write(content, :blob)

      index.add(path: path, oid: oid, mode: mode)
    when :delete
      index.remove(path)
    end
  end

  def normalize_path(path)
    pathname = Gitlab::Git::PathHelper.normalize_path(path)

    if pathname.each_filename.include?('..')
      raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
    end

    pathname.to_s
  end

1259 1260 1261 1262
  def refs_directory_exists?
    return false unless path_with_namespace

    File.exist?(File.join(path_to_repo, 'refs'))
1263
  end
1264

1265
  def cache
1266
    @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
1267
  end
1268 1269

  def tags_sorted_by_committed_date
1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281
    tags.sort_by do |tag|
      # Annotated tags can point to any object (e.g. a blob), but generally
      # tags point to a commit. If we don't have a commit, then just default
      # to putting the tag at the end of the list.
      target = tag.dereferenced_target

      if target
        target.committed_date
      else
        Time.now
      end
    end
1282
  end
D
Douwe Maan 已提交
1283 1284 1285 1286

  def keep_around_ref_name(sha)
    "refs/keep-around/#{sha}"
  end
Y
Yorick Peterse 已提交
1287 1288 1289 1290

  def repository_event(event, tags = {})
    Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
  end
1291
end