repository.rb 34.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
  class CommitError < StandardError; end
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 68 69 70
  def update_autocrlf_option
    raw_repository.autocrlf = :input if raw_repository.autocrlf != :input
  end

71
  # Return absolute path to repository
72
  def path_to_repo
73
    @path_to_repo ||= File.expand_path(
74
      File.join(@project.repository_storage_path, path_with_namespace + ".git")
75
    )
76 77
  end

78 79 80 81 82 83 84 85 86 87
  #
  # 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?
88 89 90
    return @has_visible_content unless @has_visible_content.nil?

    @has_visible_content = cache.fetch(:has_visible_content?) do
91
      branch_count > 0
92
    end
93 94
  end

95
  def commit(ref = 'HEAD')
96
    return nil unless exists?
97

98 99 100 101 102 103
    commit =
      if ref.is_a?(Gitlab::Git::Commit)
        ref
      else
        Gitlab::Git::Commit.find(raw_repository, ref)
      end
104

105
    commit = ::Commit.new(commit, @project) if commit
106
    commit
107
  rescue Rugged::OdbError, Rugged::TreeError
108
    nil
109 110
  end

111
  def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
112
    options = {
113 114 115 116 117
      repo: raw_repository,
      ref: ref,
      path: path,
      limit: limit,
      offset: offset,
118 119
      after: after,
      before: before,
120 121
      # --follow doesn't play well with --skip. See:
      # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
122 123
      follow: false,
      skip_merges: skip_merges
124 125 126
    }

    commits = Gitlab::Git::Commit.where(options)
127
    commits = Commit.decorate(commits, @project) if commits.present?
128 129 130
    commits
  end

131 132
  def commits_between(from, to)
    commits = Gitlab::Git::Commit.between(raw_repository, from, to)
133
    commits = Commit.decorate(commits, @project) if commits.present?
134 135 136
    commits
  end

137
  def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
138 139 140 141
    unless exists? && has_visible_content? && query.present?
      return []
    end

142 143
    ref ||= root_ref

144 145 146 147
    args = %W(
      #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset}
      --max-count #{limit} --grep=#{query} --regexp-ignore-case
    )
148
    args = args.concat(%W(-- #{path})) if path.present?
149

150 151
    git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
    git_log_results.map { |c| commit(c.chomp) }.compact
152 153
  end

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
  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)
169 170 171
  end

  def find_tag(name)
172
    tags.find { |tag| tag.name == name }
173 174
  end

175
  def add_branch(user, branch_name, target)
176 177 178 179 180 181
    oldrev = Gitlab::Git::BLANK_SHA
    ref    = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
    target = commit(target).try(:id)

    return false unless target

182
    GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
183
      update_ref!(ref, target, oldrev)
184
    end
185

186
    after_create_branch
187
    find_branch(branch_name)
188 189
  end

190 191 192 193 194 195
  def add_tag(user, tag_name, target, message = nil)
    oldrev = Gitlab::Git::BLANK_SHA
    ref    = Gitlab::Git::TAG_REF_PREFIX + tag_name
    target = commit(target).try(:id)

    return false unless target
196

197 198
    options = { message: message, tagger: user_to_committer(user) } if message

199 200 201 202 203 204
    rugged.tags.create(tag_name, target, options)
    tag = find_tag(tag_name)

    GitHooksService.new.execute(user, path_to_repo, oldrev, tag.target, ref) do
      # we already created a tag, because we need tag SHA to pass correct
      # values to hooks
205
    end
206

207 208 209 210
    tag
  rescue GitHooksService::PreReceiveError
    rugged.tags.delete(tag_name)
    raise
211 212
  end

213
  def rm_branch(user, branch_name)
214
    before_remove_branch
215

216
    branch = find_branch(branch_name)
217
    oldrev = branch.try(:dereferenced_target).try(:id)
218 219 220 221
    newrev = Gitlab::Git::BLANK_SHA
    ref    = Gitlab::Git::BRANCH_REF_PREFIX + branch_name

    GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
222
      update_ref!(ref, newrev, oldrev)
223
    end
224

225
    after_remove_branch
226
    true
227 228
  end

229
  def rm_tag(tag_name)
230
    before_remove_tag
231

Robert Schilling's avatar
Robert Schilling committed
232 233 234 235 236 237
    begin
      rugged.tags.delete(tag_name)
      true
    rescue Rugged::ReferenceError
      false
    end
238 239
  end

240 241 242 243
  def ref_names
    branch_names + tag_names
  end

244 245 246 247
  def branch_exists?(branch_name)
    branch_names.include?(branch_name)
  end

248 249
  def ref_exists?(ref)
    rugged.references.exist?(ref)
250 251
  rescue Rugged::ReferenceError
    false
252 253
  end

254 255 256 257 258
  def update_ref!(name, newrev, oldrev)
    # We use 'git update-ref' because libgit2/rugged currently does not
    # offer 'compare and swap' ref updates. Without compare-and-swap we can
    # (and have!) accidentally reset the ref to an earlier state, clobbering
    # commits. See also https://github.com/libgit2/libgit2/issues/1534.
259
    command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
260
    _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
261 262 263 264 265
      stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
    end

    return if status.zero?

266
    raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
267 268
  end

269 270 271 272
  # 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.
273 274 275 276 277
  def keep_around(sha)
    return unless sha && commit(sha)

    return if kept_around?(sha)

278 279 280 281 282
    # 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}"
283 284 285
    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}"
286
    end
287 288 289
  end

  def kept_around?(sha)
290
    ref_exists?(keep_around_ref_name(sha))
291 292
  end

293
  def diverging_commit_counts(branch)
294
    root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
Jeff Stubler's avatar
Jeff Stubler committed
295
    cache.fetch(:"diverging_commit_counts_#{branch.name}") do
296 297
      # Rugged seems to throw a `ReferenceError` when given branch_names rather
      # than SHA-1 hashes
298
      number_commits_behind = raw_repository.
299
        count_commits_between(branch.dereferenced_target.sha, root_ref_hash)
300 301

      number_commits_ahead = raw_repository.
302
        count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
303

304 305 306
      { behind: number_commits_behind, ahead: number_commits_ahead }
    end
  end
307

308 309 310
  def expire_tags_cache
    expire_method_caches(%i(tag_names tag_count))
    @tags = nil
311
  end
312

313 314 315
  def expire_branches_cache
    expire_method_caches(%i(branch_names branch_count))
    @local_branches = nil
316 317
  end

318 319
  def expire_statistics_caches
    expire_method_caches(%i(size commit_count))
320 321
  end

322 323
  def expire_all_method_caches
    expire_method_caches(CACHED_METHODS)
324 325
  end

326 327 328 329 330 331 332 333 334
  # 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
335 336
  end

337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
  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
352
    end
353

354
    expire_method_caches(to_refresh)
355

356
    to_refresh.each { |method| send(method) }
357
  end
358

359 360 361 362 363 364 365 366 367 368 369 370
  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}")
371
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
372 373
  end

374
  def expire_root_ref_cache
375
    expire_method_caches(%i(root_ref))
376 377
  end

378 379
  # Expires the cache(s) used to determine if a repository is empty or not.
  def expire_emptiness_caches
380
    return unless empty?
381

382
    expire_method_caches(%i(empty?))
383 384 385
    expire_has_visible_content_cache
  end

386 387 388 389 390
  def expire_has_visible_content_cache
    cache.expire(:has_visible_content?)
    @has_visible_content = nil
  end

391 392 393 394
  def lookup_cache
    @lookup_cache ||= {}
  end

395
  def expire_exists_cache
396
    expire_method_caches(%i(exists?))
397 398
  end

399 400 401 402 403 404 405
  # 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
406
    expire_statistics_caches
407 408
  end

409 410 411
  # Runs code after a repository has been created.
  def after_create
    expire_exists_cache
412 413
    expire_root_ref_cache
    expire_emptiness_caches
Yorick Peterse's avatar
Yorick Peterse committed
414 415

    repository_event(:create_repository)
416 417
  end

418 419
  # Runs code just before a repository is deleted.
  def before_delete
420
    expire_exists_cache
421 422
    expire_all_method_caches
    expire_branch_cache if exists?
423
    expire_content_cache
Yorick Peterse's avatar
Yorick Peterse committed
424 425

    repository_event(:remove_repository)
426 427 428 429 430 431 432
  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
Yorick Peterse's avatar
Yorick Peterse committed
433 434

    repository_event(:change_default_branch)
435 436
  end

437 438
  # Runs code before pushing (= creating or removing) a tag.
  def before_push_tag
439 440
    expire_statistics_caches
    expire_emptiness_caches
441
    expire_tags_cache
Yorick Peterse's avatar
Yorick Peterse committed
442 443

    repository_event(:push_tag)
444 445 446 447 448
  end

  # Runs code before removing a tag.
  def before_remove_tag
    expire_tags_cache
449
    expire_statistics_caches
Yorick Peterse's avatar
Yorick Peterse committed
450 451

    repository_event(:remove_tag)
452 453
  end

454
  def before_import
455
    expire_content_cache
456 457
  end

458 459
  # Runs code after a repository has been forked/imported.
  def after_import
460
    expire_content_cache
461 462
    expire_tags_cache
    expire_branches_cache
463 464 465
  end

  # Runs code after a new commit has been pushed.
466 467 468
  def after_push_commit(branch_name)
    expire_statistics_caches
    expire_branch_cache(branch_name)
Yorick Peterse's avatar
Yorick Peterse committed
469 470

    repository_event(:push_commit, branch: branch_name)
471 472 473 474
  end

  # Runs code after a new branch has been created.
  def after_create_branch
475
    expire_branches_cache
476
    expire_has_visible_content_cache
Yorick Peterse's avatar
Yorick Peterse committed
477 478

    repository_event(:push_branch)
479 480
  end

481 482 483
  # Runs code before removing an existing branch.
  def before_remove_branch
    expire_branches_cache
Yorick Peterse's avatar
Yorick Peterse committed
484 485

    repository_event(:remove_branch)
486 487
  end

488 489 490
  # Runs code after an existing branch has been removed.
  def after_remove_branch
    expire_has_visible_content_cache
491
    expire_branches_cache
492 493
  end

494
  def method_missing(m, *args, &block)
495 496 497 498 499 500
    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
501 502
  end

503 504
  def respond_to_missing?(method, include_private = false)
    raw_repository.respond_to?(method, include_private) || super
505
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
506 507

  def blob_at(sha, path)
508
    unless Gitlab::Git.blank_ref?(sha)
509
      Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
510
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
511
  end
512

513 514 515 516
  def blob_by_oid(oid)
    Gitlab::Git::Blob.raw(self, oid)
  end

517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
  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

575
  def readme
576 577 578
    if head = tree(:head)
      head.readme
    end
579
  end
580
  cache_method :readme
581

582
  def version
583
    file_on_head(:version)
584
  end
585
  cache_method :version
586

587
  def contribution_guide
588
    file_on_head(:contributing)
589
  end
590
  cache_method :contribution_guide
591 592

  def changelog
593
    file_on_head(:changelog)
594
  end
595
  cache_method :changelog
596

597
  def license_blob
598
    file_on_head(:license)
599
  end
600
  cache_method :license_blob
601

602
  def license_key
603
    return unless exists?
604

605
    Licensee.license(path).try(:key)
606
  end
607
  cache_method :license_key
608

609
  def gitignore
610
    file_on_head(:gitignore)
611
  end
612
  cache_method :gitignore
613

614
  def koding_yml
615
    file_on_head(:koding)
616
  end
617
  cache_method :koding_yml
618

619
  def gitlab_ci_yml
620
    file_on_head(:gitlab_ci)
621
  end
622
  cache_method :gitlab_ci_yml
623

624
  def head_commit
625 626 627 628
    @head_commit ||= commit(self.root_ref)
  end

  def head_tree
629 630 631
    if head_commit
      @head_tree ||= Tree.new(self, head_commit.sha, nil)
    end
632 633
  end

634
  def tree(sha = :head, path = nil, recursive: false)
635
    if sha == :head
636 637
      return unless head_commit

638 639 640 641 642
      if path.nil?
        return head_tree
      else
        sha = head_commit.sha
      end
643 644
    end

645
    Tree.new(self, sha, path, recursive: recursive)
646
  end
647 648

  def blob_at_branch(branch_name, path)
649
    last_commit = commit(branch_name)
650

651 652 653 654 655
    if last_commit
      blob_at(last_commit.sha, path)
    else
      nil
    end
656
  end
657 658 659 660 661 662 663 664

  # Returns url for submodule
  #
  # Ex.
  #   @repository.submodule_url_for('master', 'rack')
  #   # => git@localhost:rack.git
  #
  def submodule_url_for(ref, path)
665
    if submodules(ref).any?
666 667 668 669 670 671 672
      submodule = submodules(ref)[path]

      if submodule
        submodule['url']
      end
    end
  end
673 674

  def last_commit_for_path(sha, path)
675
    args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
676 677
    sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
    commit(sha)
678
  end
679

680
  def next_branch(name, opts = {})
681 682 683
    branch_ids = self.branch_names.map do |n|
      next 1 if n == name
      result = n.match(/\A#{name}-([0-9]+)\z/)
684 685 686
      result[1].to_i if result
    end.compact

687
    highest_branch_id = branch_ids.max || 0
688

689 690 691
    return name if opts[:mild] && 0 == highest_branch_id

    "#{name}-#{highest_branch_id + 1}"
692 693
  end

694
  # Remove archives older than 2 hours
695 696
  def branches_sorted_by(value)
    case value
697 698
    when 'name'
      branches.sort_by(&:name)
699
    when 'updated_desc'
700
      branches.sort do |a, b|
701
        commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
702
      end
703
    when 'updated_asc'
704
      branches.sort do |a, b|
705
        commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
706 707 708 709 710
      end
    else
      branches
    end
  end
711

712 713 714
  def tags_sorted_by(value)
    case value
    when 'name'
715
      VersionSorter.rsort(tags) { |tag| tag.name }
716 717 718 719 720 721 722 723 724
    when 'updated_desc'
      tags_sorted_by_committed_date.reverse
    when 'updated_asc'
      tags_sorted_by_committed_date
    else
      tags
    end
  end

725
  def contributors
726
    commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
727

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
728
    commits.group_by(&:author_email).map do |email, commits|
729 730
      contributor = Gitlab::Contributor.new
      contributor.email = email
731

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
732
      commits.each do |commit|
733
        if contributor.name.blank?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
734
          contributor.name = commit.author_name
735 736
        end

737
        contributor.commits += 1
738 739
      end

740 741
      contributor
    end
742
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
743

744 745
  def ref_name_for_sha(ref_path, sha)
    args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
746 747 748 749 750 751

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

752 753
  def refs_contains_sha(ref_type, sha)
    args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
754 755 756 757 758 759 760 761 762 763 764 765 766 767
    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
768

769 770 771
  def branch_names_contains(sha)
    refs_contains_sha('branch', sha)
  end
772

773 774
  def tag_names_contains(sha)
    refs_contains_sha('tag', sha)
775
  end
776

777
  def local_branches
778
    @local_branches ||= raw_repository.local_branches
779 780
  end

781 782
  alias_method :branches, :local_branches

783 784 785 786
  def tags
    @tags ||= raw_repository.tags
  end

787
  # rubocop:disable Metrics/ParameterLists
Lin Jen-Shin's avatar
Lin Jen-Shin committed
788 789
  def commit_dir(
    user, path, message, branch,
790 791
    author_email: nil, author_name: nil,
    source_branch: nil, source_project: project)
792 793 794
    update_branch_with_hooks(
      user,
      branch,
795 796
      source_branch: source_branch,
      source_project: source_project) do |ref|
797 798 799 800 801 802
      options = {
        commit: {
          branch: ref,
          message: message,
          update_ref: false
        }
Stan Hu's avatar
Stan Hu committed
803 804
      }

805 806
      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))

Stan Hu's avatar
Stan Hu committed
807 808 809
      raw_repository.mkdir(path, options)
    end
  end
810
  # rubocop:enable Metrics/ParameterLists
811

Lin Jen-Shin's avatar
Lin Jen-Shin committed
812 813 814
  # rubocop:disable Metrics/ParameterLists
  def commit_file(
    user, path, content, message, branch, update,
815 816
    author_email: nil, author_name: nil,
    source_branch: nil, source_project: project)
817 818 819
    update_branch_with_hooks(
      user,
      branch,
820 821
      source_branch: source_branch,
      source_project: source_project) do |ref|
822 823 824 825 826 827 828 829 830 831 832
      options = {
        commit: {
          branch: ref,
          message: message,
          update_ref: false
        },
        file: {
          content: content,
          path: path,
          update: update
        }
833
      }
834

835
      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
836

837 838
      Gitlab::Git::Blob.commit(raw_repository, options)
    end
839
  end
Lin Jen-Shin's avatar
Lin Jen-Shin committed
840
  # rubocop:enable Metrics/ParameterLists
841

Lin Jen-Shin's avatar
Lin Jen-Shin committed
842 843 844
  # rubocop:disable Metrics/ParameterLists
  def update_file(
    user, path, content,
845
    branch:, previous_path:, message:,
846 847
    author_email: nil, author_name: nil,
    source_branch: nil, source_project: project)
848 849 850
    update_branch_with_hooks(
      user,
      branch,
851 852
      source_branch: source_branch,
      source_project: source_project) do |ref|
853 854 855 856 857 858 859 860 861 862 863
      options = {
        commit: {
          branch: ref,
          message: message,
          update_ref: false
        },
        file: {
          content: content,
          path: path,
          update: true
        }
864 865
      }

866
      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
867

868
      if previous_path && previous_path != path
869
        options[:file][:previous_path] = previous_path
870
        Gitlab::Git::Blob.rename(raw_repository, options)
tiagonbotelho's avatar
tiagonbotelho committed
871
      else
872
        Gitlab::Git::Blob.commit(raw_repository, options)
tiagonbotelho's avatar
tiagonbotelho committed
873
      end
874 875
    end
  end
Lin Jen-Shin's avatar
Lin Jen-Shin committed
876
  # rubocop:enable Metrics/ParameterLists
877

878
  # rubocop:disable Metrics/ParameterLists
Lin Jen-Shin's avatar
Lin Jen-Shin committed
879 880
  def remove_file(
    user, path, message, branch,
881 882
    author_email: nil, author_name: nil,
    source_branch: nil, source_project: project)
883 884 885
    update_branch_with_hooks(
      user,
      branch,
886 887
      source_branch: source_branch,
      source_project: source_project) do |ref|
888 889 890 891 892 893 894 895 896
      options = {
        commit: {
          branch: ref,
          message: message,
          update_ref: false
        },
        file: {
          path: path
        }
897
      }
898

899
      options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
900

901 902
      Gitlab::Git::Blob.remove(raw_repository, options)
    end
903
  end
904
  # rubocop:enable Metrics/ParameterLists
905

906
  # rubocop:disable Metrics/ParameterLists
Lin Jen-Shin's avatar
Lin Jen-Shin committed
907 908
  def multi_action(
    user:, branch:, message:, actions:,
909 910
    author_email: nil, author_name: nil,
    source_branch: nil, source_project: project)
911 912 913
    update_branch_with_hooks(
      user,
      branch,
914 915
      source_branch: source_branch,
      source_project: source_project) do |ref|
Marc Siegfriedt's avatar
Marc Siegfriedt committed
916
      index = rugged.index
917 918 919 920 921 922 923 924 925
      branch_commit = find_branch(ref)

      parents = if branch_commit
                  last_commit = branch_commit.dereferenced_target
                  index.read_tree(last_commit.raw_commit.tree)
                  [last_commit.sha]
                else
                  []
                end
Marc Siegfriedt's avatar
Marc Siegfriedt committed
926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959

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

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

          content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
          oid = rugged.write(content, :blob)

          index.add(path: action[:file_path], oid: oid, mode: mode)
        when :delete
          index.remove(action[:file_path])
        end
      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
960
  # rubocop:enable Metrics/ParameterLists
Marc Siegfriedt's avatar
Marc Siegfriedt committed
961

962 963
  def get_committer_and_author(user, email: nil, name: nil)
    committer = user_to_committer(user)
964
    author = Gitlab::Git::committer_hash(email: email, name: name) || committer
965

966
    {
967 968
      author: author,
      committer: committer
969 970 971
    }
  end

972 973 974 975
  def user_to_committer(user)
    Gitlab::Git::committer_hash(email: user.email, name: user.name)
  end

976 977 978 979 980 981 982 983 984 985 986
  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

987 988 989
  def merge(user, merge_request, options = {})
    our_commit = rugged.branches[merge_request.target_branch].target
    their_commit = rugged.lookup(merge_request.diff_head_sha)
990 991 992 993 994 995 996

    raise "Invalid merge target" if our_commit.nil?
    raise "Invalid merge source" if their_commit.nil?

    merge_index = rugged.merge_commits(our_commit, their_commit)
    return false if merge_index.conflicts?

997
    update_branch_with_hooks(user, merge_request.target_branch) do
998 999 1000 1001
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
      )
1002

1003 1004 1005
      commit_id = Rugged::Commit.create(rugged, actual_options)
      merge_request.update(in_progress_merge_commit_sha: commit_id)
      commit_id
1006
    end
1007 1008
  end

1009
  def revert(user, commit, base_branch, revert_tree_id = nil)
1010
    revert_tree_id ||= check_revert_content(commit, base_branch)
1011

1012
    return false unless revert_tree_id
1013

1014 1015 1016
    update_branch_with_hooks(
      user,
      base_branch,
1017 1018 1019
      source_commit: commit) do

      source_sha = find_branch(base_branch).dereferenced_target.sha
1020
      committer = user_to_committer(user)
1021

Lin Jen-Shin's avatar
Lin Jen-Shin committed
1022
      Rugged::Commit.create(rugged,
1023
        message: commit.revert_message,
1024 1025
        author: committer,
        committer: committer,
1026
        tree: revert_tree_id,
1027
        parents: [rugged.lookup(source_sha)])
1028
    end
1029 1030
  end

1031 1032 1033 1034 1035
  def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
    cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)

    return false unless cherry_pick_tree_id

1036 1037 1038
    update_branch_with_hooks(
      user,
      base_branch,
1039 1040 1041
      source_commit: commit) do

      source_sha = find_branch(base_branch).dereferenced_target.sha
1042
      committer = user_to_committer(user)
1043

Lin Jen-Shin's avatar
Lin Jen-Shin committed
1044
      Rugged::Commit.create(rugged,
1045 1046 1047 1048 1049 1050 1051 1052
        message: commit.message,
        author: {
          email: commit.author_email,
          name: commit.author_name,
          time: commit.authored_date
        },
        committer: committer,
        tree: cherry_pick_tree_id,
1053
        parents: [rugged.lookup(source_sha)])
1054 1055 1056
    end
  end

1057
  def resolve_conflicts(user, branch, params)
1058
    update_branch_with_hooks(user, branch) do
1059 1060 1061 1062 1063 1064
      committer = user_to_committer(user)

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

1065
  def check_revert_content(commit, base_branch)
1066
    source_sha = find_branch(base_branch).dereferenced_target.sha
1067
    args       = [commit.id, source_sha]
1068
    args << { mainline: 1 } if commit.merge_commit?
1069 1070 1071 1072 1073 1074 1075 1076 1077 1078

    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

1079
  def check_cherry_pick_content(commit, base_branch)
1080
    source_sha = find_branch(base_branch).dereferenced_target.sha
1081
    args       = [commit.id, source_sha]
1082
    args << 1 if commit.merge_commit?
1083 1084 1085 1086 1087 1088 1089 1090 1091 1092

    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

1093 1094
  def diff_exists?(sha1, sha2)
    rugged.diff(sha1, sha2).size > 0
1095 1096
  end

1097 1098 1099 1100 1101
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

    if branch_commit
1102 1103
      same_head = branch_commit.id == root_ref_commit.id
      !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
1104 1105 1106 1107 1108
    else
      nil
    end
  end

1109
  def merge_base(first_commit_id, second_commit_id)
1110 1111
    first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
    second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
1112
    rugged.merge_base(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
1113 1114
  rescue Rugged::ReferenceError
    nil
1115 1116
  end

1117 1118 1119 1120
  def is_ancestor?(ancestor_id, descendant_id)
    merge_base(ancestor_id, descendant_id) == ancestor_id
  end

1121 1122 1123 1124 1125 1126
  def empty_repo?
    !exists? || !has_visible_content?
  end

  def search_files_by_content(query, ref)
    return [] if empty_repo? || query.blank?
Valery Sizov's avatar
Valery Sizov committed
1127

1128
    offset = 2
1129
    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})
1130 1131 1132
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

1133 1134 1135 1136 1137 1138 1139
  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

1140
  def fetch_ref(source_path, source_ref, target_ref)
1141
    args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
1142 1143 1144
    Gitlab::Popen.popen(args, path_to_repo)
  end

1145 1146 1147 1148
  def create_ref(ref, ref_path)
    fetch_ref(path_to_repo, ref, ref_path)
  end

1149 1150 1151 1152 1153
  # Whenever `source_branch` or `source_commit` is passed, if `branch`
  # doesn't exist, it would be created from `source_branch` or
  # `source_commit`. Should only pass one of them, not both.
  # If `source_project` is passed, and the branch doesn't exist,
  # it would try to find the source from it instead of current repository.
1154 1155 1156
  def update_branch_with_hooks(
    current_user, branch,
    source_branch: nil, source_commit: nil, source_project: project)
1157 1158
    update_autocrlf_option

1159
    target_branch, new_branch_added =
1160 1161 1162
      raw_ensure_branch(
        branch,
        source_branch: source_branch,
1163
        source_commit: source_commit,
1164 1165 1166 1167
        source_project: source_project
      )

    ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
1168
    was_empty = empty?
1169

1170 1171
    # Make commit
    newrev = yield(ref)
1172

1173 1174 1175
    unless newrev
      raise CommitError.new('Failed to create commit')
    end
1176

1177
    if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil?
1178 1179
      oldrev = Gitlab::Git::BLANK_SHA
    else
1180
      oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha)
1181
    end
1182

1183
    GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
1184
      update_ref!(ref, newrev, oldrev)
1185

1186 1187 1188
      # If repo was empty expire cache
      after_create if was_empty
      after_create_branch if was_empty || new_branch_added
1189
    end
1190 1191

    newrev
1192 1193
  end

1194 1195 1196 1197 1198
  def ls_files(ref)
    actual_ref = ref || root_ref
    raw_repository.ls_files(actual_ref)
  end

1199 1200 1201 1202
  def gitattribute(path, name)
    raw_repository.attributes(path)[name]
  end

1203 1204 1205 1206 1207 1208 1209 1210 1211 1212
  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

1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225
  # 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)
1226

1227 1228 1229 1230 1231 1232 1233 1234 1235
    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
1236 1237 1238 1239
      end
    end
  end

1240 1241 1242
  def cache_instance_variable_name(key)
    :"@#{key.to_s.tr('?!', '')}"
  end
1243

1244 1245 1246 1247
  def file_on_head(type)
    if head = tree(:head)
      head.blobs.find do |file|
        Gitlab::FileDetector.type_of(file.name) == type
1248 1249
      end
    end
1250
  end
1251

1252 1253
  private

1254 1255 1256 1257
  def refs_directory_exists?
    return false unless path_with_namespace

    File.exist?(File.join(path_to_repo, 'refs'))
1258
  end
1259

1260
  def cache
1261
    @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
1262
  end
1263 1264

  def tags_sorted_by_committed_date
1265
    tags.sort_by { |tag| tag.dereferenced_target.committed_date }
1266
  end
1267 1268 1269 1270

  def keep_around_ref_name(sha)
    "refs/keep-around/#{sha}"
  end
Yorick Peterse's avatar
Yorick Peterse committed
1271 1272 1273 1274

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

1276 1277
  def raw_ensure_branch(
    branch_name, source_commit: nil, source_branch: nil, source_project: nil)
1278 1279
    old_branch = find_branch(branch_name)

1280 1281 1282 1283 1284
    if source_commit && source_branch
      raise ArgumentError,
        'Should only pass either :source_branch or :source_commit, not both'
    end

1285 1286
    if old_branch
      [old_branch, false]
1287 1288 1289 1290 1291 1292 1293 1294 1295 1296
    elsif project != source_project
      unless source_branch
        raise ArgumentError,
          'Should also pass :source_branch if' +
          ' :source_project is different from current project'
      end

      fetch_ref(
        source_project.repository.path_to_repo,
        "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}",
1297
        "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}"
1298 1299 1300
      )

      [find_branch(branch_name), true]
1301
    elsif source_commit || source_branch
1302 1303
      oldrev = Gitlab::Git::BLANK_SHA
      ref    = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
1304
      target = (source_commit || commit(source_branch)).try(:sha)
1305 1306 1307

      unless target
        raise CommitError.new(
1308
          "Cannot find branch #{branch_name} nor #{source_commit.try(:sha) || source_branch}")
1309 1310 1311 1312 1313 1314
      end

      update_ref!(ref, target, oldrev)

      [find_branch(branch_name), true]
    else
1315
      [nil, true] # Empty branch
1316 1317
    end
  end
1318
end