repository.rb 31.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 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 741
    source_branch_name: nil, source_project: project)
    if branch_exists?(branch_name)
L
Lin Jen-Shin 已提交
742
      # tree_entry is private
743
      entry = raw_repository.send(:tree_entry, commit(branch_name), path)
L
Lin Jen-Shin 已提交
744 745 746 747 748 749 750 751 752 753 754 755 756 757

      if 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
    end

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

L
Lin Jen-Shin 已提交
770 771
  # rubocop:disable Metrics/ParameterLists
  def commit_file(
772 773
    user, path, content,
    message:, branch_name:, update: true,
774
    author_email: nil, author_name: nil,
775 776
    source_branch_name: nil, source_project: project)
    if branch_exists?(branch_name) && update == false
777
      # tree_entry is private
778
      if raw_repository.send(:tree_entry, commit(branch_name), path)
779 780 781 782 783
        raise Gitlab::Git::Repository::InvalidBlobName.new(
          "Filename already exists; update not allowed")
      end
    end

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

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

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

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

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

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

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

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

882
    {
883 884
      author: author,
      committer: committer
885 886 887
    }
  end

888
  def user_to_committer(user)
889
    Gitlab::Git.committer_hash(email: user.email, name: user.name)
890 891
  end

892 893 894 895 896 897 898 899 900 901 902
  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

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

909 910
      raise 'Invalid merge target' unless our_commit
      raise 'Invalid merge source' unless their_commit
911

912 913
      merge_index = rugged.merge_commits(our_commit, their_commit)
      break if merge_index.conflicts?
914

915 916 917 918
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
      )
919

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

928
  def revert(
929 930 931
    user, commit, branch_name, revert_tree_id = nil,
    source_branch_name: nil, source_project: project)
    revert_tree_id ||= check_revert_content(commit, branch_name)
932

933
    return false unless revert_tree_id
934

935
    GitOperationService.new(user, self).with_branch(
936 937
      branch_name,
      source_branch_name: source_branch_name,
938
      source_project: source_project) do |source_commit|
939

940
      committer = user_to_committer(user)
941

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

951
  def cherry_pick(
952 953 954
    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 已提交
955 956 957

    return false unless cherry_pick_tree_id

958
    GitOperationService.new(user, self).with_branch(
959 960
      branch_name,
      source_branch_name: source_branch_name,
961
      source_project: source_project) do |source_commit|
962

P
P.S.V.R 已提交
963
      committer = user_to_committer(user)
964

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

978 979
  def resolve_conflicts(user, branch_name, params)
    GitOperationService.new(user, self).with_branch(branch_name) do
980 981 982 983 984 985
      committer = user_to_committer(user)

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

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

    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

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

    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

1014 1015
  def diff_exists?(sha1, sha2)
    rugged.diff(sha1, sha2).size > 0
1016 1017
  end

F
Florent (HP) 已提交
1018 1019 1020 1021 1022
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

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

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

1038 1039 1040 1041
  def is_ancestor?(ancestor_id, descendant_id)
    merge_base(ancestor_id, descendant_id) == ancestor_id
  end

V
Valery Sizov 已提交
1042 1043 1044 1045 1046 1047
  def empty_repo?
    !exists? || !has_visible_content?
  end

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

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

V
Valery Sizov 已提交
1054 1055 1056 1057 1058 1059 1060
  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

1061
  def with_tmp_ref(source_repository, source_branch_name)
1062
    tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
1063 1064 1065 1066

    fetch_ref(
      source_repository.path_to_repo,
      "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}",
1067
      tmp_ref
1068 1069 1070 1071 1072
    )

    yield

  ensure
1073
    rugged.references.delete(tmp_ref)
1074 1075
  end

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

1081 1082 1083 1084
  def create_ref(ref, ref_path)
    fetch_ref(path_to_repo, ref, ref_path)
  end

1085 1086 1087 1088 1089
  def ls_files(ref)
    actual_ref = ref || root_ref
    raw_repository.ls_files(actual_ref)
  end

1090 1091 1092 1093
  def gitattribute(path, name)
    raw_repository.attributes(path)[name]
  end

1094 1095 1096 1097 1098 1099 1100 1101 1102 1103
  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

1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
  # 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)
1117

1118 1119 1120 1121 1122 1123 1124 1125 1126
    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
1127 1128 1129 1130
      end
    end
  end

1131 1132 1133
  def cache_instance_variable_name(key)
    :"@#{key.to_s.tr('?!', '')}"
  end
1134

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

1143 1144
  private

1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 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 1182 1183 1184 1185 1186 1187 1188
  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

1189 1190 1191 1192
  def refs_directory_exists?
    return false unless path_with_namespace

    File.exist?(File.join(path_to_repo, 'refs'))
1193
  end
1194

1195
  def cache
1196
    @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
1197
  end
1198 1199

  def tags_sorted_by_committed_date
1200
    tags.sort_by { |tag| tag.dereferenced_target.committed_date }
1201
  end
D
Douwe Maan 已提交
1202 1203 1204 1205

  def keep_around_ref_name(sha)
    "refs/keep-around/#{sha}"
  end
Y
Yorick Peterse 已提交
1206 1207 1208 1209

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