repository.rb 31.6 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

199
  # TODO: why we don't pass user here?
200
  def rm_tag(tag_name)
Y
Yorick Peterse 已提交
201
    before_remove_tag
202

R
Robert Schilling 已提交
203 204 205 206 207 208
    begin
      rugged.tags.delete(tag_name)
      true
    rescue Rugged::ReferenceError
      false
    end
209 210
  end

211 212 213 214
  def ref_names
    branch_names + tag_names
  end

215 216 217 218
  def branch_exists?(branch_name)
    branch_names.include?(branch_name)
  end

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

D
Douwe Maan 已提交
225 226 227 228
  # 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.
229 230 231 232 233
  def keep_around(sha)
    return unless sha && commit(sha)

    return if kept_around?(sha)

234 235 236 237 238
    # 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}"
239 240 241
    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}"
242
    end
243 244 245
  end

  def kept_around?(sha)
246
    ref_exists?(keep_around_ref_name(sha))
247 248
  end

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

      number_commits_ahead = raw_repository.
258
        count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
259

260 261 262
      { behind: number_commits_behind, ahead: number_commits_ahead }
    end
  end
263

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

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

274 275
  def expire_statistics_caches
    expire_method_caches(%i(size commit_count))
276 277
  end

278 279
  def expire_all_method_caches
    expire_method_caches(CACHED_METHODS)
D
Douwe Maan 已提交
280 281
  end

282 283 284 285 286 287 288 289 290
  # 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 已提交
291 292
  end

293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
  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
308
    end
309

310
    expire_method_caches(to_refresh)
311

312
    to_refresh.each { |method| send(method) }
313
  end
314

315 316 317 318 319 320 321 322 323 324 325 326
  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}")
327
    end
D
Dmitriy Zaporozhets 已提交
328 329
  end

330
  def expire_root_ref_cache
331
    expire_method_caches(%i(root_ref))
332 333
  end

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

338
    expire_method_caches(%i(empty?))
339 340
  end

341 342 343 344
  def lookup_cache
    @lookup_cache ||= {}
  end

345
  def expire_exists_cache
346
    expire_method_caches(%i(exists?))
347 348
  end

349 350 351 352 353 354 355
  # 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
356
    expire_statistics_caches
357 358
  end

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

    repository_event(:create_repository)
366 367
  end

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

    repository_event(:remove_repository)
376 377 378 379 380 381 382
  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 已提交
383 384

    repository_event(:change_default_branch)
385 386
  end

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

    repository_event(:push_tag)
Y
Yorick Peterse 已提交
394 395 396 397 398
  end

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

    repository_event(:remove_tag)
402 403
  end

404
  def before_import
405
    expire_content_cache
406 407
  end

408 409
  # Runs code after a repository has been forked/imported.
  def after_import
410
    expire_content_cache
411 412
    expire_tags_cache
    expire_branches_cache
413 414 415
  end

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

    repository_event(:push_commit, branch: branch_name)
421 422 423 424
  end

  # Runs code after a new branch has been created.
  def after_create_branch
425
    expire_branches_cache
Y
Yorick Peterse 已提交
426 427

    repository_event(:push_branch)
428 429
  end

430 431 432
  # Runs code before removing an existing branch.
  def before_remove_branch
    expire_branches_cache
Y
Yorick Peterse 已提交
433 434

    repository_event(:remove_branch)
435 436
  end

437 438
  # Runs code after an existing branch has been removed.
  def after_remove_branch
439
    expire_branches_cache
440 441
  end

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

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

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

461 462 463 464
  def blob_by_oid(oid)
    Gitlab::Git::Blob.raw(self, oid)
  end

465 466 467 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
  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

523
  def readme
524 525 526
    if head = tree(:head)
      head.readme
    end
527
  end
528
  cache_method :readme
529

530
  def version
531
    file_on_head(:version)
532
  end
533
  cache_method :version
534

535
  def contribution_guide
536
    file_on_head(:contributing)
537
  end
538
  cache_method :contribution_guide
539 540

  def changelog
541
    file_on_head(:changelog)
542
  end
543
  cache_method :changelog
544

545
  def license_blob
546
    file_on_head(:license)
547
  end
548
  cache_method :license_blob
Z
Zeger-Jan van de Weg 已提交
549

550
  def license_key
551
    return unless exists?
552

553
    Licensee.license(path).try(:key)
554
  end
555
  cache_method :license_key
556

557
  def gitignore
558
    file_on_head(:gitignore)
559
  end
560
  cache_method :gitignore
561

562
  def koding_yml
563
    file_on_head(:koding)
564
  end
565
  cache_method :koding_yml
566

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

572
  def head_commit
573 574 575 576
    @head_commit ||= commit(self.root_ref)
  end

  def head_tree
577 578 579
    if head_commit
      @head_tree ||= Tree.new(self, head_commit.sha, nil)
    end
580 581
  end

582
  def tree(sha = :head, path = nil, recursive: false)
583
    if sha == :head
584 585
      return unless head_commit

586 587 588 589 590
      if path.nil?
        return head_tree
      else
        sha = head_commit.sha
      end
591 592
    end

593
    Tree.new(self, sha, path, recursive: recursive)
594
  end
D
Dmitriy Zaporozhets 已提交
595 596

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

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

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

      if submodule
        submodule['url']
      end
    end
  end
621 622

  def last_commit_for_path(sha, path)
623
    args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
624 625
    sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
    commit(sha)
626
  end
627

628
  def next_branch(name, opts = {})
P
P.S.V.R 已提交
629 630 631
    branch_ids = self.branch_names.map do |n|
      next 1 if n == name
      result = n.match(/\A#{name}-([0-9]+)\z/)
632 633 634
      result[1].to_i if result
    end.compact

P
P.S.V.R 已提交
635
    highest_branch_id = branch_ids.max || 0
636

P
P.S.V.R 已提交
637 638 639
    return name if opts[:mild] && 0 == highest_branch_id

    "#{name}-#{highest_branch_id + 1}"
640 641
  end

642
  # Remove archives older than 2 hours
643 644
  def branches_sorted_by(value)
    case value
645 646
    when 'name'
      branches.sort_by(&:name)
647
    when 'updated_desc'
648
      branches.sort do |a, b|
649
        commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
650
      end
651
    when 'updated_asc'
652
      branches.sort do |a, b|
653
        commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
654 655 656 657 658
      end
    else
      branches
    end
  end
659

660 661 662
  def tags_sorted_by(value)
    case value
    when 'name'
663
      VersionSorter.rsort(tags) { |tag| tag.name }
664 665 666 667 668 669 670 671 672
    when 'updated_desc'
      tags_sorted_by_committed_date.reverse
    when 'updated_asc'
      tags_sorted_by_committed_date
    else
      tags
    end
  end

673
  def contributors
674
    commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
675

D
Dmitriy Zaporozhets 已提交
676
    commits.group_by(&:author_email).map do |email, commits|
677 678
      contributor = Gitlab::Contributor.new
      contributor.email = email
679

D
Dmitriy Zaporozhets 已提交
680
      commits.each do |commit|
681
        if contributor.name.blank?
D
Dmitriy Zaporozhets 已提交
682
          contributor.name = commit.author_name
683 684
        end

685
        contributor.commits += 1
686 687
      end

688 689
      contributor
    end
690
  end
D
Dmitriy Zaporozhets 已提交
691

692 693
  def ref_name_for_sha(ref_path, sha)
    args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
694 695 696 697 698 699

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

700 701
  def refs_contains_sha(ref_type, sha)
    args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
702 703 704 705 706 707 708 709 710 711 712 713 714 715
    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 已提交
716

717 718 719
  def branch_names_contains(sha)
    refs_contains_sha('branch', sha)
  end
H
Hannes Rosenögger 已提交
720

721 722
  def tag_names_contains(sha)
    refs_contains_sha('tag', sha)
H
Hannes Rosenögger 已提交
723
  end
724

725
  def local_branches
726
    @local_branches ||= raw_repository.local_branches
727 728
  end

729 730
  alias_method :branches, :local_branches

731 732 733 734
  def tags
    @tags ||= raw_repository.tags
  end

735
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
736
  def commit_dir(
737 738
    user, path,
    message:, branch_name:,
739
    author_email: nil, author_name: nil,
740
    source_branch_name: nil, source_project: project)
741 742 743 744 745
    check_tree_entry_for_dir(branch_name, path)

    if source_branch_name
      source_project.repository.
        check_tree_entry_for_dir(source_branch_name, path)
L
Lin Jen-Shin 已提交
746 747 748 749
    end

    commit_file(
      user,
750
      "#{path}/.gitkeep",
L
Lin Jen-Shin 已提交
751
      '',
752 753 754
      message: message,
      branch_name: branch_name,
      update: false,
L
Lin Jen-Shin 已提交
755 756
      author_email: author_email,
      author_name: author_name,
757
      source_branch_name: source_branch_name,
L
Lin Jen-Shin 已提交
758
      source_project: source_project)
S
Stan Hu 已提交
759
  end
760
  # rubocop:enable Metrics/ParameterLists
761

L
Lin Jen-Shin 已提交
762 763
  # rubocop:disable Metrics/ParameterLists
  def commit_file(
764 765
    user, path, content,
    message:, branch_name:, update: true,
766
    author_email: nil, author_name: nil,
767
    source_branch_name: nil, source_project: project)
768 769 770 771 772 773 774 775 776 777
    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

      if source_branch_name &&
         source_project.repository.tree_entry_at(source_branch_name, path)
        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
778 779 780
      end
    end

781 782 783
    multi_action(
      user: user,
      message: message,
784
      branch_name: branch_name,
785 786
      author_email: author_email,
      author_name: author_name,
787
      source_branch_name: source_branch_name,
788
      source_project: source_project,
L
Lin Jen-Shin 已提交
789 790 791
      actions: [{ action: :create,
                  file_path: path,
                  content: content }])
792
  end
L
Lin Jen-Shin 已提交
793
  # rubocop:enable Metrics/ParameterLists
794

L
Lin Jen-Shin 已提交
795 796 797
  # rubocop:disable Metrics/ParameterLists
  def update_file(
    user, path, content,
798
    message:, branch_name:, previous_path:,
799
    author_email: nil, author_name: nil,
800
    source_branch_name: nil, source_project: project)
801 802 803 804 805 806 807 808 809
    action = if previous_path && previous_path != path
               :move
             else
               :update
             end

    multi_action(
      user: user,
      message: message,
810
      branch_name: branch_name,
811 812
      author_email: author_email,
      author_name: author_name,
813
      source_branch_name: source_branch_name,
814
      source_project: source_project,
L
Lin Jen-Shin 已提交
815 816 817 818
      actions: [{ action: action,
                  file_path: path,
                  content: content,
                  previous_path: previous_path }])
819
  end
L
Lin Jen-Shin 已提交
820
  # rubocop:enable Metrics/ParameterLists
821

822
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
823
  def remove_file(
824 825
    user, path,
    message:, branch_name:,
826
    author_email: nil, author_name: nil,
827
    source_branch_name: nil, source_project: project)
828 829 830
    multi_action(
      user: user,
      message: message,
831
      branch_name: branch_name,
832 833
      author_email: author_email,
      author_name: author_name,
834
      source_branch_name: source_branch_name,
835
      source_project: source_project,
L
Lin Jen-Shin 已提交
836 837
      actions: [{ action: :delete,
                  file_path: path }])
838
  end
839
  # rubocop:enable Metrics/ParameterLists
840

841
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
842
  def multi_action(
843
    user:, branch_name:, message:, actions:,
844
    author_email: nil, author_name: nil,
845
    source_branch_name: nil, source_project: project)
846
    GitOperationService.new(user, self).with_branch(
847 848
      branch_name,
      source_branch_name: source_branch_name,
849
      source_project: source_project) do |source_commit|
M
Marc Siegfriedt 已提交
850
      index = rugged.index
851

852 853 854
      parents = if source_commit
                  index.read_tree(source_commit.raw_commit.tree)
                  [source_commit.sha]
855 856 857
                else
                  []
                end
M
Marc Siegfriedt 已提交
858

859 860
      actions.each do |act|
        git_action(index, act)
M
Marc Siegfriedt 已提交
861 862 863 864 865 866 867 868 869 870 871 872
      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
873
  # rubocop:enable Metrics/ParameterLists
M
Marc Siegfriedt 已提交
874

875 876
  def get_committer_and_author(user, email: nil, name: nil)
    committer = user_to_committer(user)
D
Dan Dunckel 已提交
877
    author = Gitlab::Git::committer_hash(email: email, name: name) || committer
878

879
    {
880 881
      author: author,
      committer: committer
882 883 884
    }
  end

885
  def user_to_committer(user)
886
    Gitlab::Git.committer_hash(email: user.email, name: user.name)
887 888
  end

889 890 891 892 893 894 895 896 897 898 899
  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

900
  def merge(user, merge_request, options = {})
901 902
    GitOperationService.new(user, self).with_branch(
      merge_request.target_branch) do |source_commit|
903 904
      our_commit = source_commit.sha
      their_commit = merge_request.diff_head_sha
905

906 907
      raise 'Invalid merge target' unless our_commit
      raise 'Invalid merge source' unless their_commit
908

909 910
      merge_index = rugged.merge_commits(our_commit, their_commit)
      break if merge_index.conflicts?
911

912 913 914 915
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
      )
916

917 918 919
      commit_id = Rugged::Commit.create(rugged, actual_options)
      merge_request.update(in_progress_merge_commit_sha: commit_id)
      commit_id
920
    end
921 922
  rescue Repository::CommitError # when merge_index.conflicts?
    false
923 924
  end

925
  def revert(
926 927 928
    user, commit, branch_name, revert_tree_id = nil,
    source_branch_name: nil, source_project: project)
    revert_tree_id ||= check_revert_content(commit, branch_name)
929

930
    return false unless revert_tree_id
931

932
    GitOperationService.new(user, self).with_branch(
933 934
      branch_name,
      source_branch_name: source_branch_name,
935
      source_project: source_project) do |source_commit|
936

937
      committer = user_to_committer(user)
938

L
Lin Jen-Shin 已提交
939
      Rugged::Commit.create(rugged,
940
        message: commit.revert_message(user),
941 942
        author: committer,
        committer: committer,
943
        tree: revert_tree_id,
944
        parents: [source_commit.sha])
945
    end
946 947
  end

948
  def cherry_pick(
949 950 951
    user, commit, branch_name, cherry_pick_tree_id = nil,
    source_branch_name: nil, source_project: project)
    cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
P
P.S.V.R 已提交
952 953 954

    return false unless cherry_pick_tree_id

955
    GitOperationService.new(user, self).with_branch(
956 957
      branch_name,
      source_branch_name: source_branch_name,
958
      source_project: source_project) do |source_commit|
959

P
P.S.V.R 已提交
960
      committer = user_to_committer(user)
961

L
Lin Jen-Shin 已提交
962
      Rugged::Commit.create(rugged,
P
P.S.V.R 已提交
963 964 965 966 967 968 969 970
        message: commit.message,
        author: {
          email: commit.author_email,
          name: commit.author_name,
          time: commit.authored_date
        },
        committer: committer,
        tree: cherry_pick_tree_id,
971
        parents: [source_commit.sha])
P
P.S.V.R 已提交
972 973 974
    end
  end

975 976
  def resolve_conflicts(user, branch_name, params)
    GitOperationService.new(user, self).with_branch(branch_name) do
977 978 979 980 981 982
      committer = user_to_committer(user)

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

983 984
  def check_revert_content(commit, branch_name)
    source_sha = find_branch(branch_name).dereferenced_target.sha
985
    args       = [commit.id, source_sha]
986
    args << { mainline: 1 } if commit.merge_commit?
987 988 989 990 991 992 993 994 995 996

    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

997 998
  def check_cherry_pick_content(commit, branch_name)
    source_sha = find_branch(branch_name).dereferenced_target.sha
P
P.S.V.R 已提交
999
    args       = [commit.id, source_sha]
1000
    args << 1 if commit.merge_commit?
P
P.S.V.R 已提交
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010

    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

1011 1012
  def diff_exists?(sha1, sha2)
    rugged.diff(sha1, sha2).size > 0
1013 1014
  end

F
Florent (HP) 已提交
1015 1016 1017 1018 1019
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

    if branch_commit
1020 1021
      same_head = branch_commit.id == root_ref_commit.id
      !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
F
Florent (HP) 已提交
1022 1023 1024 1025 1026
    else
      nil
    end
  end

S
Stan Hu 已提交
1027
  def merge_base(first_commit_id, second_commit_id)
1028 1029
    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 已提交
1030
    rugged.merge_base(first_commit_id, second_commit_id)
D
Douwe Maan 已提交
1031 1032
  rescue Rugged::ReferenceError
    nil
S
Stan Hu 已提交
1033 1034
  end

1035 1036 1037 1038
  def is_ancestor?(ancestor_id, descendant_id)
    merge_base(ancestor_id, descendant_id) == ancestor_id
  end

V
Valery Sizov 已提交
1039 1040 1041 1042 1043 1044
  def empty_repo?
    !exists? || !has_visible_content?
  end

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

1046
    offset = 2
1047
    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})
1048 1049 1050
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

V
Valery Sizov 已提交
1051 1052 1053 1054 1055 1056 1057
  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

1058
  def with_tmp_ref(source_repository, source_branch_name)
1059
    tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
1060 1061 1062 1063

    fetch_ref(
      source_repository.path_to_repo,
      "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}",
1064
      tmp_ref
1065 1066 1067 1068 1069
    )

    yield

  ensure
1070
    rugged.references.delete(tmp_ref)
1071 1072
  end

1073
  def fetch_ref(source_path, source_ref, target_ref)
1074
    args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
1075 1076 1077
    Gitlab::Popen.popen(args, path_to_repo)
  end

1078 1079 1080 1081
  def create_ref(ref, ref_path)
    fetch_ref(path_to_repo, ref, ref_path)
  end

1082 1083 1084 1085 1086
  def ls_files(ref)
    actual_ref = ref || root_ref
    raw_repository.ls_files(actual_ref)
  end

1087 1088 1089 1090
  def gitattribute(path, name)
    raw_repository.attributes(path)[name]
  end

1091 1092 1093 1094 1095 1096 1097 1098 1099 1100
  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

1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113
  # 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)
1114

1115 1116 1117 1118 1119 1120 1121 1122 1123
    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
1124 1125 1126 1127
      end
    end
  end

1128 1129 1130
  def cache_instance_variable_name(key)
    :"@#{key.to_s.tr('?!', '')}"
  end
1131

1132 1133 1134 1135
  def file_on_head(type)
    if head = tree(:head)
      head.blobs.find do |file|
        Gitlab::FileDetector.type_of(file.name) == type
1136 1137
      end
    end
1138
  end
1139

1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
  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

1164 1165
  private

1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 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
  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

1210 1211 1212 1213
  def refs_directory_exists?
    return false unless path_with_namespace

    File.exist?(File.join(path_to_repo, 'refs'))
1214
  end
1215

1216
  def cache
1217
    @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
1218
  end
1219 1220

  def tags_sorted_by_committed_date
1221
    tags.sort_by { |tag| tag.dereferenced_target.committed_date }
1222
  end
D
Douwe Maan 已提交
1223 1224 1225 1226

  def keep_around_ref_name(sha)
    "refs/keep-around/#{sha}"
  end
Y
Yorick Peterse 已提交
1227 1228 1229 1230

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