repository.rb 31.4 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 85 86
    return @has_visible_content unless @has_visible_content.nil?

    @has_visible_content = cache.fetch(:has_visible_content?) do
87
      branch_count > 0
88
    end
89 90
  end

L
Lin Jen-Shin 已提交
91
  def commit(ref = 'HEAD')
92
    return nil unless exists?
93

94 95 96 97 98 99
    commit =
      if ref.is_a?(Gitlab::Git::Commit)
        ref
      else
        Gitlab::Git::Commit.find(raw_repository, ref)
      end
100

101
    commit = ::Commit.new(commit, @project) if commit
102
    commit
103
  rescue Rugged::OdbError, Rugged::TreeError
104
    nil
105 106
  end

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

    commits = Gitlab::Git::Commit.where(options)
123
    commits = Commit.decorate(commits, @project) if commits.present?
124 125 126
    commits
  end

127 128
  def commits_between(from, to)
    commits = Gitlab::Git::Commit.between(raw_repository, from, to)
129
    commits = Commit.decorate(commits, @project) if commits.present?
130 131 132
    commits
  end

133
  def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
134 135 136 137
    unless exists? && has_visible_content? && query.present?
      return []
    end

138 139
    ref ||= root_ref

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

146 147
    git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
    git_log_results.map { |c| commit(c.chomp) }.compact
148 149
  end

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
  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)
165 166 167
  end

  def find_tag(name)
168
    tags.find { |tag| tag.name == name }
169 170
  end

171 172
  def add_branch(user, branch_name, ref)
    newrev = commit(ref).try(:sha)
173

174
    return false unless newrev
175

176
    GitOperationService.new(user, self).add_branch(branch_name, newrev)
177

178
    after_create_branch
179
    find_branch(branch_name)
180 181
  end

182
  def add_tag(user, tag_name, target, message = nil)
183
    newrev = commit(target).try(:id)
184 185
    options = { message: message, tagger: user_to_committer(user) } if message

186 187 188
    return false unless newrev

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

190
    find_tag(tag_name)
191 192
  end

193
  def rm_branch(user, branch_name)
194
    before_remove_branch
195 196
    branch = find_branch(branch_name)

197
    GitOperationService.new(user, self).rm_branch(branch)
198

199
    after_remove_branch
200
    true
201 202
  end

203
  # TODO: why we don't pass user here?
204
  def rm_tag(tag_name)
Y
Yorick Peterse 已提交
205
    before_remove_tag
206

R
Robert Schilling 已提交
207 208 209 210 211 212
    begin
      rugged.tags.delete(tag_name)
      true
    rescue Rugged::ReferenceError
      false
    end
213 214
  end

215 216 217 218
  def ref_names
    branch_names + tag_names
  end

219 220 221 222
  def branch_exists?(branch_name)
    branch_names.include?(branch_name)
  end

223 224
  def ref_exists?(ref)
    rugged.references.exist?(ref)
225 226
  rescue Rugged::ReferenceError
    false
227 228
  end

D
Douwe Maan 已提交
229 230 231 232
  # 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.
233 234 235 236 237
  def keep_around(sha)
    return unless sha && commit(sha)

    return if kept_around?(sha)

238 239 240 241 242
    # 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}"
243 244 245
    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}"
246
    end
247 248 249
  end

  def kept_around?(sha)
250
    ref_exists?(keep_around_ref_name(sha))
251 252
  end

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

      number_commits_ahead = raw_repository.
262
        count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
263

264 265 266
      { behind: number_commits_behind, ahead: number_commits_ahead }
    end
  end
267

268 269 270
  def expire_tags_cache
    expire_method_caches(%i(tag_names tag_count))
    @tags = nil
271
  end
272

273 274 275
  def expire_branches_cache
    expire_method_caches(%i(branch_names branch_count))
    @local_branches = nil
276 277
  end

278 279
  def expire_statistics_caches
    expire_method_caches(%i(size commit_count))
280 281
  end

282 283
  def expire_all_method_caches
    expire_method_caches(CACHED_METHODS)
D
Douwe Maan 已提交
284 285
  end

286 287 288 289 290 291 292 293 294
  # 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 已提交
295 296
  end

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
  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
312
    end
313

314
    expire_method_caches(to_refresh)
315

316
    to_refresh.each { |method| send(method) }
317
  end
318

319 320 321 322 323 324 325 326 327 328 329 330
  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}")
331
    end
D
Dmitriy Zaporozhets 已提交
332 333
  end

334
  def expire_root_ref_cache
335
    expire_method_caches(%i(root_ref))
336 337
  end

338 339
  # Expires the cache(s) used to determine if a repository is empty or not.
  def expire_emptiness_caches
340
    return unless empty?
341

342
    expire_method_caches(%i(empty?))
343 344 345
    expire_has_visible_content_cache
  end

346 347 348 349 350
  def expire_has_visible_content_cache
    cache.expire(:has_visible_content?)
    @has_visible_content = nil
  end

351 352 353 354
  def lookup_cache
    @lookup_cache ||= {}
  end

355
  def expire_exists_cache
356
    expire_method_caches(%i(exists?))
357 358
  end

359 360 361 362 363 364 365
  # 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
366
    expire_statistics_caches
367 368
  end

369 370 371
  # Runs code after a repository has been created.
  def after_create
    expire_exists_cache
372 373
    expire_root_ref_cache
    expire_emptiness_caches
Y
Yorick Peterse 已提交
374 375

    repository_event(:create_repository)
376 377
  end

378 379
  # Runs code just before a repository is deleted.
  def before_delete
380
    expire_exists_cache
381 382
    expire_all_method_caches
    expire_branch_cache if exists?
383
    expire_content_cache
Y
Yorick Peterse 已提交
384 385

    repository_event(:remove_repository)
386 387 388 389 390 391 392
  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 已提交
393 394

    repository_event(:change_default_branch)
395 396
  end

Y
Yorick Peterse 已提交
397 398
  # Runs code before pushing (= creating or removing) a tag.
  def before_push_tag
399 400
    expire_statistics_caches
    expire_emptiness_caches
401
    expire_tags_cache
Y
Yorick Peterse 已提交
402 403

    repository_event(:push_tag)
Y
Yorick Peterse 已提交
404 405 406 407 408
  end

  # Runs code before removing a tag.
  def before_remove_tag
    expire_tags_cache
409
    expire_statistics_caches
Y
Yorick Peterse 已提交
410 411

    repository_event(:remove_tag)
412 413
  end

414
  def before_import
415
    expire_content_cache
416 417
  end

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

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

    repository_event(:push_commit, branch: branch_name)
431 432 433 434
  end

  # Runs code after a new branch has been created.
  def after_create_branch
435
    expire_branches_cache
436
    expire_has_visible_content_cache
Y
Yorick Peterse 已提交
437 438

    repository_event(:push_branch)
439 440
  end

441 442 443
  # Runs code before removing an existing branch.
  def before_remove_branch
    expire_branches_cache
Y
Yorick Peterse 已提交
444 445

    repository_event(:remove_branch)
446 447
  end

448 449 450
  # Runs code after an existing branch has been removed.
  def after_remove_branch
    expire_has_visible_content_cache
451
    expire_branches_cache
452 453
  end

454
  def method_missing(m, *args, &block)
455 456 457 458 459 460
    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
461 462
  end

463 464
  def respond_to_missing?(method, include_private = false)
    raw_repository.respond_to?(method, include_private) || super
465
  end
D
Dmitriy Zaporozhets 已提交
466 467

  def blob_at(sha, path)
468
    unless Gitlab::Git.blank_ref?(sha)
469
      Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
470
    end
D
Dmitriy Zaporozhets 已提交
471
  end
472

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

477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
  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

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

542
  def version
543
    file_on_head(:version)
544
  end
545
  cache_method :version
546

547
  def contribution_guide
548
    file_on_head(:contributing)
549
  end
550
  cache_method :contribution_guide
551 552

  def changelog
553
    file_on_head(:changelog)
554
  end
555
  cache_method :changelog
556

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

562
  def license_key
563
    return unless exists?
564

565
    Licensee.license(path).try(:key)
566
  end
567
  cache_method :license_key
568

569
  def gitignore
570
    file_on_head(:gitignore)
571
  end
572
  cache_method :gitignore
573

574
  def koding_yml
575
    file_on_head(:koding)
576
  end
577
  cache_method :koding_yml
578

579
  def gitlab_ci_yml
580
    file_on_head(:gitlab_ci)
581
  end
582
  cache_method :gitlab_ci_yml
583

584
  def head_commit
585 586 587 588
    @head_commit ||= commit(self.root_ref)
  end

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

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

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

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

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

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

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

      if submodule
        submodule['url']
      end
    end
  end
633 634

  def last_commit_for_path(sha, path)
635
    args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
636 637
    sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
    commit(sha)
638
  end
639

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

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

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

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

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

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

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

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

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

697
        contributor.commits += 1
698 699
      end

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

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

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

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

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

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

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

741 742
  alias_method :branches, :local_branches

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

747
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
748
  def commit_dir(
749 750
    user, path,
    message:, branch_name:,
751
    author_email: nil, author_name: nil,
752 753
    source_branch_name: nil, source_project: project)
    if branch_exists?(branch_name)
L
Lin Jen-Shin 已提交
754
      # tree_entry is private
755
      entry = raw_repository.send(:tree_entry, commit(branch_name), path)
L
Lin Jen-Shin 已提交
756 757 758 759 760 761 762 763 764 765 766 767 768 769

      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,
770
      "#{path}/.gitkeep",
L
Lin Jen-Shin 已提交
771
      '',
772 773 774
      message: message,
      branch_name: branch_name,
      update: false,
L
Lin Jen-Shin 已提交
775 776
      author_email: author_email,
      author_name: author_name,
777
      source_branch_name: source_branch_name,
L
Lin Jen-Shin 已提交
778
      source_project: source_project)
S
Stan Hu 已提交
779
  end
780
  # rubocop:enable Metrics/ParameterLists
781

L
Lin Jen-Shin 已提交
782 783
  # rubocop:disable Metrics/ParameterLists
  def commit_file(
784 785
    user, path, content,
    message:, branch_name:, update: true,
786
    author_email: nil, author_name: nil,
787 788
    source_branch_name: nil, source_project: project)
    if branch_exists?(branch_name) && update == false
789
      # tree_entry is private
790
      if raw_repository.send(:tree_entry, commit(branch_name), path)
791 792 793 794 795
        raise Gitlab::Git::Repository::InvalidBlobName.new(
          "Filename already exists; update not allowed")
      end
    end

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

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

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

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

856
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
857
  def multi_action(
858
    user:, branch_name:, message:, actions:,
859
    author_email: nil, author_name: nil,
860
    source_branch_name: nil, source_project: project)
861
    GitOperationService.new(user, self).with_branch(
862 863
      branch_name,
      source_branch_name: source_branch_name,
864
      source_project: source_project) do
M
Marc Siegfriedt 已提交
865
      index = rugged.index
866
      branch_commit = source_project.repository.find_branch(
867
        source_branch_name || branch_name)
868 869 870 871 872 873 874 875

      parents = if branch_commit
                  last_commit = branch_commit.dereferenced_target
                  index.read_tree(last_commit.raw_commit.tree)
                  [last_commit.sha]
                else
                  []
                end
M
Marc Siegfriedt 已提交
876 877

      actions.each do |action|
878 879
        path = Gitlab::Git::PathHelper.normalize_path(action[:file_path]).to_s

880 881 882
        raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if
          path.split('/').include?('..')

M
Marc Siegfriedt 已提交
883 884 885 886 887
        case action[:action]
        when :create, :update, :move
          mode =
            case action[:action]
            when :update
888
              index.get(path)[:mode]
M
Marc Siegfriedt 已提交
889 890 891 892 893 894 895 896 897 898
            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)

899
          index.add(path: path, oid: oid, mode: mode)
M
Marc Siegfriedt 已提交
900
        when :delete
901
          index.remove(path)
M
Marc Siegfriedt 已提交
902 903 904 905 906 907 908 909 910 911 912 913 914
        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
915
  # rubocop:enable Metrics/ParameterLists
M
Marc Siegfriedt 已提交
916

917 918
  def get_committer_and_author(user, email: nil, name: nil)
    committer = user_to_committer(user)
D
Dan Dunckel 已提交
919
    author = Gitlab::Git::committer_hash(email: email, name: name) || committer
920

921
    {
922 923
      author: author,
      committer: committer
924 925 926
    }
  end

927
  def user_to_committer(user)
928
    Gitlab::Git.committer_hash(email: user.email, name: user.name)
929 930
  end

931 932 933 934 935 936 937 938 939 940 941
  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

942 943 944
  def merge(user, merge_request, options = {})
    our_commit = rugged.branches[merge_request.target_branch].target
    their_commit = rugged.lookup(merge_request.diff_head_sha)
945 946 947 948 949 950 951

    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?

952 953
    GitOperationService.new(user, self).with_branch(
      merge_request.target_branch) do
954 955 956 957
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
      )
958

959 960 961
      commit_id = Rugged::Commit.create(rugged, actual_options)
      merge_request.update(in_progress_merge_commit_sha: commit_id)
      commit_id
962
    end
963 964
  end

965
  def revert(
966 967 968
    user, commit, branch_name, revert_tree_id = nil,
    source_branch_name: nil, source_project: project)
    revert_tree_id ||= check_revert_content(commit, branch_name)
969

970
    return false unless revert_tree_id
971

972
    GitOperationService.new(user, self).with_branch(
973 974 975
      branch_name,
      source_branch_name: source_branch_name,
      source_project: source_project) do
976

977
      source_sha = source_project.repository.find_source_sha(
978
        source_branch_name || branch_name)
979
      committer = user_to_committer(user)
980

L
Lin Jen-Shin 已提交
981
      Rugged::Commit.create(rugged,
R
Rubén Dávila 已提交
982
        message: commit.revert_message,
983 984
        author: committer,
        committer: committer,
985
        tree: revert_tree_id,
986
        parents: [rugged.lookup(source_sha)])
987
    end
988 989
  end

990
  def cherry_pick(
991 992 993
    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 已提交
994 995 996

    return false unless cherry_pick_tree_id

997
    GitOperationService.new(user, self).with_branch(
998 999 1000
      branch_name,
      source_branch_name: source_branch_name,
      source_project: source_project) do
1001

1002
      source_sha = source_project.repository.find_source_sha(
1003
        source_branch_name || branch_name)
P
P.S.V.R 已提交
1004
      committer = user_to_committer(user)
1005

L
Lin Jen-Shin 已提交
1006
      Rugged::Commit.create(rugged,
P
P.S.V.R 已提交
1007 1008 1009 1010 1011 1012 1013 1014
        message: commit.message,
        author: {
          email: commit.author_email,
          name: commit.author_name,
          time: commit.authored_date
        },
        committer: committer,
        tree: cherry_pick_tree_id,
1015
        parents: [rugged.lookup(source_sha)])
P
P.S.V.R 已提交
1016 1017 1018
    end
  end

1019 1020
  def resolve_conflicts(user, branch_name, params)
    GitOperationService.new(user, self).with_branch(branch_name) do
1021 1022 1023 1024 1025 1026
      committer = user_to_committer(user)

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

1027 1028
  def check_revert_content(commit, branch_name)
    source_sha = find_branch(branch_name).dereferenced_target.sha
1029
    args       = [commit.id, source_sha]
1030
    args << { mainline: 1 } if commit.merge_commit?
1031 1032 1033 1034 1035 1036 1037 1038 1039 1040

    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

1041 1042
  def check_cherry_pick_content(commit, branch_name)
    source_sha = find_branch(branch_name).dereferenced_target.sha
P
P.S.V.R 已提交
1043
    args       = [commit.id, source_sha]
1044
    args << 1 if commit.merge_commit?
P
P.S.V.R 已提交
1045 1046 1047 1048 1049 1050 1051 1052 1053 1054

    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

1055 1056
  def diff_exists?(sha1, sha2)
    rugged.diff(sha1, sha2).size > 0
1057 1058
  end

F
Florent (HP) 已提交
1059 1060 1061 1062 1063
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

    if branch_commit
1064 1065
      same_head = branch_commit.id == root_ref_commit.id
      !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
F
Florent (HP) 已提交
1066 1067 1068 1069 1070
    else
      nil
    end
  end

S
Stan Hu 已提交
1071
  def merge_base(first_commit_id, second_commit_id)
1072 1073
    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 已提交
1074
    rugged.merge_base(first_commit_id, second_commit_id)
D
Douwe Maan 已提交
1075 1076
  rescue Rugged::ReferenceError
    nil
S
Stan Hu 已提交
1077 1078
  end

1079 1080 1081 1082
  def is_ancestor?(ancestor_id, descendant_id)
    merge_base(ancestor_id, descendant_id) == ancestor_id
  end

V
Valery Sizov 已提交
1083 1084 1085 1086 1087 1088
  def empty_repo?
    !exists? || !has_visible_content?
  end

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

1090
    offset = 2
1091
    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})
1092 1093 1094
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

V
Valery Sizov 已提交
1095 1096 1097 1098 1099 1100 1101
  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

1102
  def fetch_ref(source_path, source_ref, target_ref)
1103
    args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
1104 1105 1106
    Gitlab::Popen.popen(args, path_to_repo)
  end

1107 1108 1109 1110
  def create_ref(ref, ref_path)
    fetch_ref(path_to_repo, ref, ref_path)
  end

1111 1112 1113 1114 1115
  def ls_files(ref)
    actual_ref = ref || root_ref
    raw_repository.ls_files(actual_ref)
  end

1116 1117 1118 1119
  def gitattribute(path, name)
    raw_repository.attributes(path)[name]
  end

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

1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142
  # 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)
1143

1144 1145 1146 1147 1148 1149 1150 1151 1152
    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
1153 1154 1155 1156
      end
    end
  end

1157 1158 1159
  def cache_instance_variable_name(key)
    :"@#{key.to_s.tr('?!', '')}"
  end
1160

1161 1162 1163 1164
  def file_on_head(type)
    if head = tree(:head)
      head.blobs.find do |file|
        Gitlab::FileDetector.type_of(file.name) == type
1165 1166
      end
    end
1167
  end
1168

1169 1170 1171 1172 1173 1174 1175 1176 1177 1178
  protected

  def find_source_sha(branch_name)
    if branch_exists?(branch_name)
      find_branch(branch_name).dereferenced_target.sha
    else
      Gitlab::Git::BLANK_SHA
    end
  end

1179 1180
  private

1181 1182 1183 1184
  def refs_directory_exists?
    return false unless path_with_namespace

    File.exist?(File.join(path_to_repo, 'refs'))
1185
  end
1186

1187
  def cache
1188
    @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
1189
  end
1190 1191

  def tags_sorted_by_committed_date
1192
    tags.sort_by { |tag| tag.dereferenced_target.committed_date }
1193
  end
D
Douwe Maan 已提交
1194 1195 1196 1197

  def keep_around_ref_name(sha)
    "refs/keep-around/#{sha}"
  end
Y
Yorick Peterse 已提交
1198 1199 1200 1201

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