repository.rb 32.1 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 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
  # 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
                      tag_count avatar exists? empty? root_ref)

  # 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
  }

  # 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
  # Runs code after a repository has been forked/imported.
  def after_import
413
    expire_content_cache
414 415
    expire_tags_cache
    expire_branches_cache
416 417 418
  end

  # Runs code after a new commit has been pushed.
419 420 421
  def after_push_commit(branch_name)
    expire_statistics_caches
    expire_branch_cache(branch_name)
Y
Yorick Peterse 已提交
422 423

    repository_event(:push_commit, branch: branch_name)
424 425 426 427
  end

  # Runs code after a new branch has been created.
  def after_create_branch
428
    expire_branches_cache
Y
Yorick Peterse 已提交
429 430

    repository_event(:push_branch)
431 432
  end

433 434 435
  # Runs code before removing an existing branch.
  def before_remove_branch
    expire_branches_cache
Y
Yorick Peterse 已提交
436 437

    repository_event(:remove_branch)
438 439
  end

440 441
  # Runs code after an existing branch has been removed.
  def after_remove_branch
442
    expire_branches_cache
443 444
  end

445
  def method_missing(m, *args, &block)
446 447 448 449 450 451
    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
452 453
  end

454 455
  def respond_to_missing?(method, include_private = false)
    raw_repository.respond_to?(method, include_private) || super
456
  end
D
Dmitriy Zaporozhets 已提交
457 458

  def blob_at(sha, path)
459
    unless Gitlab::Git.blank_ref?(sha)
460
      Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
461
    end
D
Dmitriy Zaporozhets 已提交
462
  end
463

464 465 466 467
  def blob_by_oid(oid)
    Gitlab::Git::Blob.raw(self, oid)
  end

468 469 470 471 472 473 474 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
  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

526
  def readme
527 528 529
    if head = tree(:head)
      head.readme
    end
530
  end
531
  cache_method :readme
532

533
  def version
534
    file_on_head(:version)
535
  end
536
  cache_method :version
537

538
  def contribution_guide
539
    file_on_head(:contributing)
540
  end
541
  cache_method :contribution_guide
542 543

  def changelog
544
    file_on_head(:changelog)
545
  end
546
  cache_method :changelog
547

548
  def license_blob
549
    file_on_head(:license)
550
  end
551
  cache_method :license_blob
Z
Zeger-Jan van de Weg 已提交
552

553
  def license_key
554
    return unless exists?
555

556
    Licensee.license(path).try(:key)
557
  end
558
  cache_method :license_key
559

560
  def gitignore
561
    file_on_head(:gitignore)
562
  end
563
  cache_method :gitignore
564

565
  def koding_yml
566
    file_on_head(:koding)
567
  end
568
  cache_method :koding_yml
569

570
  def gitlab_ci_yml
571
    file_on_head(:gitlab_ci)
572
  end
573
  cache_method :gitlab_ci_yml
574

575
  def head_commit
576 577 578 579
    @head_commit ||= commit(self.root_ref)
  end

  def head_tree
580 581 582
    if head_commit
      @head_tree ||= Tree.new(self, head_commit.sha, nil)
    end
583 584
  end

585
  def tree(sha = :head, path = nil, recursive: false)
586
    if sha == :head
587 588
      return unless head_commit

589 590 591 592 593
      if path.nil?
        return head_tree
      else
        sha = head_commit.sha
      end
594 595
    end

596
    Tree.new(self, sha, path, recursive: recursive)
597
  end
D
Dmitriy Zaporozhets 已提交
598 599

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

D
Dmitriy Zaporozhets 已提交
602 603 604 605 606
    if last_commit
      blob_at(last_commit.sha, path)
    else
      nil
    end
D
Dmitriy Zaporozhets 已提交
607
  end
D
Dmitriy Zaporozhets 已提交
608 609 610 611 612 613 614 615

  # Returns url for submodule
  #
  # Ex.
  #   @repository.submodule_url_for('master', 'rack')
  #   # => git@localhost:rack.git
  #
  def submodule_url_for(ref, path)
D
Dmitriy Zaporozhets 已提交
616
    if submodules(ref).any?
D
Dmitriy Zaporozhets 已提交
617 618 619 620 621 622 623
      submodule = submodules(ref)[path]

      if submodule
        submodule['url']
      end
    end
  end
624 625

  def last_commit_for_path(sha, path)
H
Hiroyuki Sato 已提交
626
    sha = last_commit_id_for_path(sha, path)
627
    commit(sha)
628
  end
629

H
Hiroyuki Sato 已提交
630 631
  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 已提交
632

H
Hiroyuki Sato 已提交
633
    cache.fetch(key) do
H
Hiroyuki Sato 已提交
634 635
      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 已提交
636 637 638
    end
  end

639
  def next_branch(name, opts = {})
P
P.S.V.R 已提交
640 641 642
    branch_ids = self.branch_names.map do |n|
      next 1 if n == name
      result = n.match(/\A#{name}-([0-9]+)\z/)
643 644 645
      result[1].to_i if result
    end.compact

P
P.S.V.R 已提交
646
    highest_branch_id = branch_ids.max || 0
647

P
P.S.V.R 已提交
648 649 650
    return name if opts[:mild] && 0 == highest_branch_id

    "#{name}-#{highest_branch_id + 1}"
651 652
  end

653
  # Remove archives older than 2 hours
654 655
  def branches_sorted_by(value)
    case value
656 657
    when 'name'
      branches.sort_by(&:name)
658
    when 'updated_desc'
659
      branches.sort do |a, b|
660
        commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
661
      end
662
    when 'updated_asc'
663
      branches.sort do |a, b|
664
        commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
665 666 667 668 669
      end
    else
      branches
    end
  end
670

671 672 673
  def tags_sorted_by(value)
    case value
    when 'name'
674
      VersionSorter.rsort(tags) { |tag| tag.name }
675 676 677 678 679 680 681 682 683
    when 'updated_desc'
      tags_sorted_by_committed_date.reverse
    when 'updated_asc'
      tags_sorted_by_committed_date
    else
      tags
    end
  end

684
  def contributors
685
    commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
686

D
Dmitriy Zaporozhets 已提交
687
    commits.group_by(&:author_email).map do |email, commits|
688 689
      contributor = Gitlab::Contributor.new
      contributor.email = email
690

D
Dmitriy Zaporozhets 已提交
691
      commits.each do |commit|
692
        if contributor.name.blank?
D
Dmitriy Zaporozhets 已提交
693
          contributor.name = commit.author_name
694 695
        end

696
        contributor.commits += 1
697 698
      end

699 700
      contributor
    end
701
  end
D
Dmitriy Zaporozhets 已提交
702

703 704
  def ref_name_for_sha(ref_path, sha)
    args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
705 706 707 708 709 710

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

711 712
  def refs_contains_sha(ref_type, sha)
    args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
713 714 715 716 717 718 719 720 721 722 723 724 725 726
    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 已提交
727

728 729 730
  def branch_names_contains(sha)
    refs_contains_sha('branch', sha)
  end
H
Hannes Rosenögger 已提交
731

732 733
  def tag_names_contains(sha)
    refs_contains_sha('tag', sha)
H
Hannes Rosenögger 已提交
734
  end
735

736
  def local_branches
737
    @local_branches ||= raw_repository.local_branches
738 739
  end

740 741
  alias_method :branches, :local_branches

742 743 744 745
  def tags
    @tags ||= raw_repository.tags
  end

746
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
747
  def commit_dir(
748 749
    user, path,
    message:, branch_name:,
750
    author_email: nil, author_name: nil,
751
    start_branch_name: nil, start_project: project)
752 753
    check_tree_entry_for_dir(branch_name, path)

754 755 756
    if start_branch_name
      start_project.repository.
        check_tree_entry_for_dir(start_branch_name, path)
L
Lin Jen-Shin 已提交
757 758 759 760
    end

    commit_file(
      user,
761
      "#{path}/.gitkeep",
L
Lin Jen-Shin 已提交
762
      '',
763 764 765
      message: message,
      branch_name: branch_name,
      update: false,
L
Lin Jen-Shin 已提交
766 767
      author_email: author_email,
      author_name: author_name,
768 769
      start_branch_name: start_branch_name,
      start_project: start_project)
S
Stan Hu 已提交
770
  end
771
  # rubocop:enable Metrics/ParameterLists
772

L
Lin Jen-Shin 已提交
773 774
  # rubocop:disable Metrics/ParameterLists
  def commit_file(
775 776
    user, path, content,
    message:, branch_name:, update: true,
777
    author_email: nil, author_name: nil,
778
    start_branch_name: nil, start_project: project)
779 780 781 782 783 784 785
    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

786 787
      if start_branch_name &&
          start_project.repository.tree_entry_at(start_branch_name, path)
788
        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
789 790 791
      end
    end

792 793 794
    multi_action(
      user: user,
      message: message,
795
      branch_name: branch_name,
796 797
      author_email: author_email,
      author_name: author_name,
798 799
      start_branch_name: start_branch_name,
      start_project: start_project,
L
Lin Jen-Shin 已提交
800 801 802
      actions: [{ action: :create,
                  file_path: path,
                  content: content }])
803
  end
L
Lin Jen-Shin 已提交
804
  # rubocop:enable Metrics/ParameterLists
805

L
Lin Jen-Shin 已提交
806 807 808
  # rubocop:disable Metrics/ParameterLists
  def update_file(
    user, path, content,
809
    message:, branch_name:, previous_path:,
810
    author_email: nil, author_name: nil,
811
    start_branch_name: nil, start_project: project)
812 813 814 815 816 817 818 819 820
    action = if previous_path && previous_path != path
               :move
             else
               :update
             end

    multi_action(
      user: user,
      message: message,
821
      branch_name: branch_name,
822 823
      author_email: author_email,
      author_name: author_name,
824 825
      start_branch_name: start_branch_name,
      start_project: start_project,
L
Lin Jen-Shin 已提交
826 827 828 829
      actions: [{ action: action,
                  file_path: path,
                  content: content,
                  previous_path: previous_path }])
830
  end
L
Lin Jen-Shin 已提交
831
  # rubocop:enable Metrics/ParameterLists
832

833
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
834
  def remove_file(
835 836
    user, path,
    message:, branch_name:,
837
    author_email: nil, author_name: nil,
838
    start_branch_name: nil, start_project: project)
839 840 841
    multi_action(
      user: user,
      message: message,
842
      branch_name: branch_name,
843 844
      author_email: author_email,
      author_name: author_name,
845 846
      start_branch_name: start_branch_name,
      start_project: start_project,
L
Lin Jen-Shin 已提交
847 848
      actions: [{ action: :delete,
                  file_path: path }])
849
  end
850
  # rubocop:enable Metrics/ParameterLists
851

852
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
853
  def multi_action(
854
    user:, branch_name:, message:, actions:,
855
    author_email: nil, author_name: nil,
856
    start_branch_name: nil, start_project: project)
857
    GitOperationService.new(user, self).with_branch(
858
      branch_name,
859 860
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
M
Marc Siegfriedt 已提交
861
      index = rugged.index
862

863 864 865
      parents = if start_commit
                  index.read_tree(start_commit.raw_commit.tree)
                  [start_commit.sha]
866 867 868
                else
                  []
                end
M
Marc Siegfriedt 已提交
869

870 871
      actions.each do |act|
        git_action(index, act)
M
Marc Siegfriedt 已提交
872 873 874 875 876 877 878 879 880 881 882 883
      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
884
  # rubocop:enable Metrics/ParameterLists
M
Marc Siegfriedt 已提交
885

886 887
  def get_committer_and_author(user, email: nil, name: nil)
    committer = user_to_committer(user)
D
Dan Dunckel 已提交
888
    author = Gitlab::Git::committer_hash(email: email, name: name) || committer
889

890
    {
891 892
      author: author,
      committer: committer
893 894 895
    }
  end

896
  def user_to_committer(user)
897
    Gitlab::Git.committer_hash(email: user.email, name: user.name)
898 899
  end

900 901 902 903 904 905 906 907 908 909 910
  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

911
  def merge(user, merge_request, options = {})
912
    GitOperationService.new(user, self).with_branch(
913 914
      merge_request.target_branch) do |start_commit|
      our_commit = start_commit.sha
915
      their_commit = merge_request.diff_head_sha
916

917 918
      raise 'Invalid merge target' unless our_commit
      raise 'Invalid merge source' unless their_commit
919

920 921
      merge_index = rugged.merge_commits(our_commit, their_commit)
      break if merge_index.conflicts?
922

923 924 925 926
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
      )
927

928 929 930
      commit_id = Rugged::Commit.create(rugged, actual_options)
      merge_request.update(in_progress_merge_commit_sha: commit_id)
      commit_id
931
    end
932 933
  rescue Repository::CommitError # when merge_index.conflicts?
    false
934 935
  end

936
  def revert(
937
    user, commit, branch_name, revert_tree_id = nil,
938
    start_branch_name: nil, start_project: project)
939
    revert_tree_id ||= check_revert_content(commit, branch_name)
940

941
    return false unless revert_tree_id
942

943
    GitOperationService.new(user, self).with_branch(
944
      branch_name,
945 946
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
947

948
      committer = user_to_committer(user)
949

L
Lin Jen-Shin 已提交
950
      Rugged::Commit.create(rugged,
951
        message: commit.revert_message(user),
952 953
        author: committer,
        committer: committer,
954
        tree: revert_tree_id,
955
        parents: [start_commit.sha])
956
    end
957 958
  end

959
  def cherry_pick(
960
    user, commit, branch_name, cherry_pick_tree_id = nil,
961
    start_branch_name: nil, start_project: project)
962
    cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
P
P.S.V.R 已提交
963 964 965

    return false unless cherry_pick_tree_id

966
    GitOperationService.new(user, self).with_branch(
967
      branch_name,
968 969
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
970

P
P.S.V.R 已提交
971
      committer = user_to_committer(user)
972

L
Lin Jen-Shin 已提交
973
      Rugged::Commit.create(rugged,
P
P.S.V.R 已提交
974 975 976 977 978 979 980 981
        message: commit.message,
        author: {
          email: commit.author_email,
          name: commit.author_name,
          time: commit.authored_date
        },
        committer: committer,
        tree: cherry_pick_tree_id,
982
        parents: [start_commit.sha])
P
P.S.V.R 已提交
983 984 985
    end
  end

986 987
  def resolve_conflicts(user, branch_name, params)
    GitOperationService.new(user, self).with_branch(branch_name) do
988 989 990 991 992 993
      committer = user_to_committer(user)

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

994 995
  def check_revert_content(commit, branch_name)
    source_sha = find_branch(branch_name).dereferenced_target.sha
996
    args       = [commit.id, source_sha]
997
    args << { mainline: 1 } if commit.merge_commit?
998 999 1000 1001 1002 1003 1004 1005 1006 1007

    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

1008 1009
  def check_cherry_pick_content(commit, branch_name)
    source_sha = find_branch(branch_name).dereferenced_target.sha
P
P.S.V.R 已提交
1010
    args       = [commit.id, source_sha]
1011
    args << 1 if commit.merge_commit?
P
P.S.V.R 已提交
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021

    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

1022 1023
  def diff_exists?(sha1, sha2)
    rugged.diff(sha1, sha2).size > 0
1024 1025
  end

F
Florent (HP) 已提交
1026 1027 1028 1029 1030
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

    if branch_commit
1031 1032
      same_head = branch_commit.id == root_ref_commit.id
      !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
F
Florent (HP) 已提交
1033 1034 1035 1036 1037
    else
      nil
    end
  end

S
Stan Hu 已提交
1038
  def merge_base(first_commit_id, second_commit_id)
1039 1040
    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 已提交
1041
    rugged.merge_base(first_commit_id, second_commit_id)
D
Douwe Maan 已提交
1042 1043
  rescue Rugged::ReferenceError
    nil
S
Stan Hu 已提交
1044 1045
  end

1046 1047 1048 1049
  def is_ancestor?(ancestor_id, descendant_id)
    merge_base(ancestor_id, descendant_id) == ancestor_id
  end

V
Valery Sizov 已提交
1050 1051 1052 1053 1054 1055
  def empty_repo?
    !exists? || !has_visible_content?
  end

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

1057
    offset = 2
1058
    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})
1059 1060 1061
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

V
Valery Sizov 已提交
1062 1063 1064 1065 1066 1067 1068
  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

1069
  def with_repo_branch_commit(start_repository, start_branch_name)
1070
    branch_name_or_sha =
1071 1072
      if start_repository == self
        start_branch_name
1073 1074
      else
        tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
1075

1076
        fetch_ref(
1077 1078
          start_repository.path_to_repo,
          "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
1079 1080 1081
          tmp_ref
        )

1082
        start_repository.commit(start_branch_name).sha
1083
      end
1084

1085
    yield(commit(branch_name_or_sha))
1086 1087

  ensure
1088
    rugged.references.delete(tmp_ref) if tmp_ref
1089 1090
  end

1091
  def fetch_ref(source_path, source_ref, target_ref)
1092
    args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
1093 1094 1095
    Gitlab::Popen.popen(args, path_to_repo)
  end

1096 1097 1098 1099
  def create_ref(ref, ref_path)
    fetch_ref(path_to_repo, ref, ref_path)
  end

1100 1101 1102 1103 1104
  def ls_files(ref)
    actual_ref = ref || root_ref
    raw_repository.ls_files(actual_ref)
  end

1105 1106 1107 1108
  def gitattribute(path, name)
    raw_repository.attributes(path)[name]
  end

1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
  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

1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131
  # 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)
1132

1133 1134 1135 1136 1137 1138 1139 1140 1141
    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
1142 1143 1144 1145
      end
    end
  end

1146 1147 1148
  def cache_instance_variable_name(key)
    :"@#{key.to_s.tr('?!', '')}"
  end
1149

1150 1151 1152 1153
  def file_on_head(type)
    if head = tree(:head)
      head.blobs.find do |file|
        Gitlab::FileDetector.type_of(file.name) == type
1154 1155
      end
    end
1156
  end
1157

1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181
  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

1182 1183
  private

1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227
  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

      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

1228 1229 1230 1231
  def refs_directory_exists?
    return false unless path_with_namespace

    File.exist?(File.join(path_to_repo, 'refs'))
1232
  end
1233

1234
  def cache
1235
    @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
1236
  end
1237 1238

  def tags_sorted_by_committed_date
1239
    tags.sort_by { |tag| tag.dereferenced_target.committed_date }
1240
  end
D
Douwe Maan 已提交
1241 1242 1243 1244

  def keep_around_ref_name(sha)
    "refs/keep-around/#{sha}"
  end
Y
Yorick Peterse 已提交
1245 1246 1247 1248

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