repository.rb 32.5 KB
Newer Older
1 2
require 'securerandom'

3
class Repository
4 5
  include Gitlab::ShellAdapter

6
  attr_accessor :path_with_namespace, :project
7

8
  CommitError = Class.new(StandardError)
9

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
  # Methods that cache data from the Git repository.
  #
  # Each entry in this Array should have a corresponding method with the exact
  # same name. The cache key used by those methods must also match method's
  # name.
  #
  # For example, for entry `:readme` there's a method called `readme` which
  # stores its data in the `readme` cache key.
  CACHED_METHODS = %i(size commit_count readme version contribution_guide
                      changelog license_blob license_key gitignore koding_yml
                      gitlab_ci_yml branch_names tag_names branch_count
                      tag_count avatar exists? empty? root_ref)

  # Certain method caches should be refreshed when certain types of files are
  # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
  # the corresponding methods to call for refreshing caches.
  METHOD_CACHES_FOR_FILE_TYPES = {
    readme: :readme,
    changelog: :changelog,
    license: %i(license_blob license_key),
    contributing: :contribution_guide,
    version: :version,
    gitignore: :gitignore,
    koding: :koding_yml,
    gitlab_ci: :gitlab_ci_yml,
    avatar: :avatar
  }

  # Wraps around the given method and caches its output in Redis and an instance
  # variable.
  #
  # This only works for methods that do not take any arguments.
  def self.cache_method(name, fallback: nil)
    original = :"_uncached_#{name}"
44

45
    alias_method(original, name)
46

47 48
    define_method(name) do
      cache_method_output(name, fallback: fallback) { __send__(original) }
49
    end
50
  end
51 52 53 54 55

  def self.storages
    Gitlab.config.repositories.storages
  end

56
  def initialize(path_with_namespace, project)
57
    @path_with_namespace = path_with_namespace
58
    @project = project
59
  end
60

61 62
  def raw_repository
    return nil unless path_with_namespace
63

64
    @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
65 66
  end

67
  # Return absolute path to repository
68
  def path_to_repo
69
    @path_to_repo ||= File.expand_path(
70
      File.join(@project.repository_storage_path, path_with_namespace + ".git")
71
    )
72 73
  end

74 75 76 77 78 79 80 81 82 83
  #
  # Git repository can contains some hidden refs like:
  #   /refs/notes/*
  #   /refs/git-as-svn/*
  #   /refs/pulls/*
  # This refs by default not visible in project page and not cloned to client side.
  #
  # This method return true if repository contains some content visible in project page.
  #
  def has_visible_content?
84
    branch_count > 0
85 86
  end

L
Lin Jen-Shin 已提交
87
  def commit(ref = 'HEAD')
88
    return nil unless exists?
89

90 91 92 93 94 95
    commit =
      if ref.is_a?(Gitlab::Git::Commit)
        ref
      else
        Gitlab::Git::Commit.find(raw_repository, ref)
      end
96

97
    commit = ::Commit.new(commit, @project) if commit
98
    commit
99
  rescue Rugged::OdbError, Rugged::TreeError
100
    nil
101 102
  end

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

    commits = Gitlab::Git::Commit.where(options)
119
    commits = Commit.decorate(commits, @project) if commits.present?
120 121 122
    commits
  end

123 124
  def commits_between(from, to)
    commits = Gitlab::Git::Commit.between(raw_repository, from, to)
125
    commits = Commit.decorate(commits, @project) if commits.present?
126 127 128
    commits
  end

129
  def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
130 131 132 133
    unless exists? && has_visible_content? && query.present?
      return []
    end

134 135
    ref ||= root_ref

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

142 143
    git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
    git_log_results.map { |c| commit(c.chomp) }.compact
144 145
  end

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
  def find_branch(name, fresh_repo: true)
    # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
    # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
    # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
    # may cause the branch to "disappear" erroneously or have the wrong SHA.
    #
    # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
    raw_repo =
      if fresh_repo
        Gitlab::Git::Repository.new(path_to_repo)
      else
        raw_repository
      end

    raw_repo.find_branch(name)
161 162 163
  end

  def find_tag(name)
164
    tags.find { |tag| tag.name == name }
165 166
  end

167 168
  def add_branch(user, branch_name, ref)
    newrev = commit(ref).try(:sha)
169

170
    return false unless newrev
171

172
    GitOperationService.new(user, self).add_branch(branch_name, newrev)
173

174
    after_create_branch
175
    find_branch(branch_name)
176 177
  end

178
  def add_tag(user, tag_name, target, message = nil)
179
    newrev = commit(target).try(:id)
180 181
    options = { message: message, tagger: user_to_committer(user) } if message

182 183 184
    return false unless newrev

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

186
    find_tag(tag_name)
187 188
  end

189
  def rm_branch(user, branch_name)
190
    before_remove_branch
191 192
    branch = find_branch(branch_name)

193
    GitOperationService.new(user, self).rm_branch(branch)
194

195
    after_remove_branch
196
    true
197 198
  end

L
Lin Jen-Shin 已提交
199
  def rm_tag(user, tag_name)
Y
Yorick Peterse 已提交
200
    before_remove_tag
L
Lin Jen-Shin 已提交
201
    tag = find_tag(tag_name)
202

L
Lin Jen-Shin 已提交
203 204 205 206
    GitOperationService.new(user, self).rm_tag(tag)

    after_remove_tag
    true
207 208
  end

209 210 211 212
  def ref_names
    branch_names + tag_names
  end

213 214 215 216
  def branch_exists?(branch_name)
    branch_names.include?(branch_name)
  end

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

D
Douwe Maan 已提交
223 224 225 226
  # Makes sure a commit is kept around when Git garbage collection runs.
  # Git GC will delete commits from the repository that are no longer in any
  # branches or tags, but we want to keep some of these commits around, for
  # example if they have comments or CI builds.
227 228 229 230 231
  def keep_around(sha)
    return unless sha && commit(sha)

    return if kept_around?(sha)

232 233 234 235 236
    # This will still fail if the file is corrupted (e.g. 0 bytes)
    begin
      rugged.references.create(keep_around_ref_name(sha), sha, force: true)
    rescue Rugged::ReferenceError => ex
      Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
237 238 239
    rescue Rugged::OSError => ex
      raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
      Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
240
    end
241 242 243
  end

  def kept_around?(sha)
244
    ref_exists?(keep_around_ref_name(sha))
245 246
  end

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

      number_commits_ahead = raw_repository.
256
        count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
257

258 259 260
      { behind: number_commits_behind, ahead: number_commits_ahead }
    end
  end
261

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

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

272 273
  def expire_statistics_caches
    expire_method_caches(%i(size commit_count))
274 275
  end

276 277
  def expire_all_method_caches
    expire_method_caches(CACHED_METHODS)
D
Douwe Maan 已提交
278 279
  end

280 281 282 283 284 285 286 287 288
  # Expires the caches of a specific set of methods
  def expire_method_caches(methods)
    methods.each do |key|
      cache.expire(key)

      ivar = cache_instance_variable_name(key)

      remove_instance_variable(ivar) if instance_variable_defined?(ivar)
    end
D
Douwe Maan 已提交
289 290
  end

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
  def expire_avatar_cache
    expire_method_caches(%i(avatar))
  end

  # Refreshes the method caches of this repository.
  #
  # types - An Array of file types (e.g. `:readme`) used to refresh extra
  #         caches.
  def refresh_method_caches(types)
    to_refresh = []

    types.each do |type|
      methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]

      to_refresh.concat(Array(methods)) if methods
306
    end
307

308
    expire_method_caches(to_refresh)
309

310
    to_refresh.each { |method| send(method) }
311
  end
312

313 314 315 316 317 318 319 320 321 322 323 324
  def expire_branch_cache(branch_name = nil)
    # When we push to the root branch we have to flush the cache for all other
    # branches as their statistics are based on the commits relative to the
    # root branch.
    if !branch_name || branch_name == root_ref
      branches.each do |branch|
        cache.expire(:"diverging_commit_counts_#{branch.name}")
      end
    # In case a commit is pushed to a non-root branch we only have to flush the
    # cache for said branch.
    else
      cache.expire(:"diverging_commit_counts_#{branch_name}")
325
    end
D
Dmitriy Zaporozhets 已提交
326 327
  end

328
  def expire_root_ref_cache
329
    expire_method_caches(%i(root_ref))
330 331
  end

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

336
    expire_method_caches(%i(empty?))
337 338
  end

339 340 341 342
  def lookup_cache
    @lookup_cache ||= {}
  end

343
  def expire_exists_cache
344
    expire_method_caches(%i(exists?))
345 346
  end

347 348 349 350 351 352 353
  # expire cache that doesn't depend on repository data (when expiring)
  def expire_content_cache
    expire_tags_cache
    expire_branches_cache
    expire_root_ref_cache
    expire_emptiness_caches
    expire_exists_cache
354
    expire_statistics_caches
355 356
  end

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

    repository_event(:create_repository)
364 365
  end

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

    repository_event(:remove_repository)
374 375 376 377 378 379 380
  end

  # Runs code just before the HEAD of a repository is changed.
  def before_change_head
    # Cached divergent commit counts are based on repository head
    expire_branch_cache
    expire_root_ref_cache
Y
Yorick Peterse 已提交
381 382

    repository_event(:change_default_branch)
383 384
  end

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

    repository_event(:push_tag)
Y
Yorick Peterse 已提交
392 393 394 395 396
  end

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

    repository_event(:remove_tag)
400 401
  end

L
Lin Jen-Shin 已提交
402 403 404 405 406
  # Runs code after removing a tag.
  def after_remove_tag
    expire_tags_cache
  end

407
  def before_import
408
    expire_content_cache
409 410
  end

411 412 413 414 415
  # Runs code after the HEAD of a repository is changed.
  def after_change_head
    expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
  end

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

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

    repository_event(:push_commit, branch: branch_name)
429 430 431 432
  end

  # Runs code after a new branch has been created.
  def after_create_branch
433
    expire_branches_cache
Y
Yorick Peterse 已提交
434 435

    repository_event(:push_branch)
436 437
  end

438 439 440
  # Runs code before removing an existing branch.
  def before_remove_branch
    expire_branches_cache
Y
Yorick Peterse 已提交
441 442

    repository_event(:remove_branch)
443 444
  end

445 446
  # Runs code after an existing branch has been removed.
  def after_remove_branch
447
    expire_branches_cache
448 449
  end

450
  def method_missing(m, *args, &block)
451 452 453 454 455 456
    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
457 458
  end

459 460
  def respond_to_missing?(method, include_private = false)
    raw_repository.respond_to?(method, include_private) || super
461
  end
D
Dmitriy Zaporozhets 已提交
462 463

  def blob_at(sha, path)
464
    unless Gitlab::Git.blank_ref?(sha)
465
      Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
466
    end
D
Dmitriy Zaporozhets 已提交
467
  end
468

469 470 471 472
  def blob_by_oid(oid)
    Gitlab::Git::Blob.raw(self, oid)
  end

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

531
  def readme
532 533 534
    if head = tree(:head)
      head.readme
    end
535
  end
536
  cache_method :readme
537

538
  def version
539
    file_on_head(:version)
540
  end
541
  cache_method :version
542

543
  def contribution_guide
544
    file_on_head(:contributing)
545
  end
546
  cache_method :contribution_guide
547 548

  def changelog
549
    file_on_head(:changelog)
550
  end
551
  cache_method :changelog
552

553
  def license_blob
554
    file_on_head(:license)
555
  end
556
  cache_method :license_blob
Z
Zeger-Jan van de Weg 已提交
557

558
  def license_key
559
    return unless exists?
560

561
    Licensee.license(path).try(:key)
562
  end
563
  cache_method :license_key
564

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

570
  def koding_yml
571
    file_on_head(:koding)
572
  end
573
  cache_method :koding_yml
574

575
  def gitlab_ci_yml
576
    file_on_head(:gitlab_ci)
577
  end
578
  cache_method :gitlab_ci_yml
579

580
  def head_commit
581 582 583 584
    @head_commit ||= commit(self.root_ref)
  end

  def head_tree
585 586 587
    if head_commit
      @head_tree ||= Tree.new(self, head_commit.sha, nil)
    end
588 589
  end

590
  def tree(sha = :head, path = nil, recursive: false)
591
    if sha == :head
592 593
      return unless head_commit

594 595 596 597 598
      if path.nil?
        return head_tree
      else
        sha = head_commit.sha
      end
599 600
    end

601
    Tree.new(self, sha, path, recursive: recursive)
602
  end
D
Dmitriy Zaporozhets 已提交
603 604

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

D
Dmitriy Zaporozhets 已提交
607 608 609 610 611
    if last_commit
      blob_at(last_commit.sha, path)
    else
      nil
    end
D
Dmitriy Zaporozhets 已提交
612
  end
D
Dmitriy Zaporozhets 已提交
613 614 615 616 617 618 619 620

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

      if submodule
        submodule['url']
      end
    end
  end
629 630

  def last_commit_for_path(sha, path)
H
Hiroyuki Sato 已提交
631
    sha = last_commit_id_for_path(sha, path)
632
    commit(sha)
633
  end
634

H
Hiroyuki Sato 已提交
635 636
  def last_commit_id_for_path(sha, path)
    key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
H
Hiroyuki Sato 已提交
637

H
Hiroyuki Sato 已提交
638
    cache.fetch(key) do
H
Hiroyuki Sato 已提交
639 640
      args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
      Gitlab::Popen.popen(args, path_to_repo).first.strip
H
Hiroyuki Sato 已提交
641 642 643
    end
  end

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

P
P.S.V.R 已提交
651
    highest_branch_id = branch_ids.max || 0
652

P
P.S.V.R 已提交
653 654 655
    return name if opts[:mild] && 0 == highest_branch_id

    "#{name}-#{highest_branch_id + 1}"
656 657
  end

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

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

689
  def contributors
690
    commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
691

D
Dmitriy Zaporozhets 已提交
692
    commits.group_by(&:author_email).map do |email, commits|
693 694
      contributor = Gitlab::Contributor.new
      contributor.email = email
695

D
Dmitriy Zaporozhets 已提交
696
      commits.each do |commit|
697
        if contributor.name.blank?
D
Dmitriy Zaporozhets 已提交
698
          contributor.name = commit.author_name
699 700
        end

701
        contributor.commits += 1
702 703
      end

704 705
      contributor
    end
706
  end
D
Dmitriy Zaporozhets 已提交
707

708 709
  def ref_name_for_sha(ref_path, sha)
    args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
710 711 712 713 714 715

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

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

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

737 738
  def tag_names_contains(sha)
    refs_contains_sha('tag', sha)
H
Hannes Rosenögger 已提交
739
  end
740

741
  def local_branches
742
    @local_branches ||= raw_repository.local_branches
743 744
  end

745 746
  alias_method :branches, :local_branches

747 748 749 750
  def tags
    @tags ||= raw_repository.tags
  end

751
  # rubocop:disable Metrics/ParameterLists
L
Lin Jen-Shin 已提交
752
  def commit_dir(
753 754
    user, path,
    message:, branch_name:,
755
    author_email: nil, author_name: nil,
756
    start_branch_name: nil, start_project: project)
757 758
    check_tree_entry_for_dir(branch_name, path)

759 760 761
    if start_branch_name
      start_project.repository.
        check_tree_entry_for_dir(start_branch_name, path)
L
Lin Jen-Shin 已提交
762 763 764 765
    end

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

L
Lin Jen-Shin 已提交
778 779
  # rubocop:disable Metrics/ParameterLists
  def commit_file(
780 781
    user, path, content,
    message:, branch_name:, update: true,
782
    author_email: nil, author_name: nil,
783
    start_branch_name: nil, start_project: project)
784 785 786 787 788 789 790
    unless update
      error_message = "Filename already exists; update not allowed"

      if tree_entry_at(branch_name, path)
        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
      end

791 792
      if start_branch_name &&
          start_project.repository.tree_entry_at(start_branch_name, path)
793
        raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
794 795 796
      end
    end

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

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

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

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

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

868 869 870
      parents = if start_commit
                  index.read_tree(start_commit.raw_commit.tree)
                  [start_commit.sha]
871 872 873
                else
                  []
                end
M
Marc Siegfriedt 已提交
874

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

891 892
  def get_committer_and_author(user, email: nil, name: nil)
    committer = user_to_committer(user)
D
Dan Dunckel 已提交
893
    author = Gitlab::Git::committer_hash(email: email, name: name) || committer
894

895
    {
896 897
      author: author,
      committer: committer
898 899 900
    }
  end

901
  def user_to_committer(user)
902
    Gitlab::Git.committer_hash(email: user.email, name: user.name)
903 904
  end

905 906 907 908 909 910 911 912 913 914 915
  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

S
Sean McGivern 已提交
916
  def merge(user, source, merge_request, options = {})
917
    GitOperationService.new(user, self).with_branch(
918 919
      merge_request.target_branch) do |start_commit|
      our_commit = start_commit.sha
S
Sean McGivern 已提交
920
      their_commit = source
921

922 923
      raise 'Invalid merge target' unless our_commit
      raise 'Invalid merge source' unless their_commit
924

925 926
      merge_index = rugged.merge_commits(our_commit, their_commit)
      break if merge_index.conflicts?
927

928 929 930 931
      actual_options = options.merge(
        parents: [our_commit, their_commit],
        tree: merge_index.write_tree(rugged),
      )
932

933 934 935
      commit_id = Rugged::Commit.create(rugged, actual_options)
      merge_request.update(in_progress_merge_commit_sha: commit_id)
      commit_id
936
    end
937 938
  rescue Repository::CommitError # when merge_index.conflicts?
    false
939 940
  end

941
  def revert(
942
    user, commit, branch_name, revert_tree_id = nil,
943
    start_branch_name: nil, start_project: project)
944
    revert_tree_id ||= check_revert_content(commit, branch_name)
945

946
    return false unless revert_tree_id
947

948
    GitOperationService.new(user, self).with_branch(
949
      branch_name,
950 951
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
952

953
      committer = user_to_committer(user)
954

L
Lin Jen-Shin 已提交
955
      Rugged::Commit.create(rugged,
956
        message: commit.revert_message(user),
957 958
        author: committer,
        committer: committer,
959
        tree: revert_tree_id,
960
        parents: [start_commit.sha])
961
    end
962 963
  end

964
  def cherry_pick(
965
    user, commit, branch_name, cherry_pick_tree_id = nil,
966
    start_branch_name: nil, start_project: project)
967
    cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
P
P.S.V.R 已提交
968 969 970

    return false unless cherry_pick_tree_id

971
    GitOperationService.new(user, self).with_branch(
972
      branch_name,
973 974
      start_branch_name: start_branch_name,
      start_project: start_project) do |start_commit|
975

P
P.S.V.R 已提交
976
      committer = user_to_committer(user)
977

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

991 992
  def resolve_conflicts(user, branch_name, params)
    GitOperationService.new(user, self).with_branch(branch_name) do
993 994 995 996 997 998
      committer = user_to_committer(user)

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

999 1000 1001 1002
  def check_revert_content(target_commit, branch_name)
    source_sha = commit(branch_name).sha
    args       = [target_commit.sha, source_sha]
    args << { mainline: 1 } if target_commit.merge_commit?
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012

    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

1013 1014 1015 1016
  def check_cherry_pick_content(target_commit, branch_name)
    source_sha = commit(branch_name).sha
    args       = [target_commit.sha, source_sha]
    args << 1 if target_commit.merge_commit?
P
P.S.V.R 已提交
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026

    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

1027 1028
  def diff_exists?(sha1, sha2)
    rugged.diff(sha1, sha2).size > 0
1029 1030
  end

F
Florent (HP) 已提交
1031 1032 1033 1034 1035
  def merged_to_root_ref?(branch_name)
    branch_commit = commit(branch_name)
    root_ref_commit = commit(root_ref)

    if branch_commit
1036 1037
      same_head = branch_commit.id == root_ref_commit.id
      !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
F
Florent (HP) 已提交
1038 1039 1040 1041 1042
    else
      nil
    end
  end

S
Stan Hu 已提交
1043
  def merge_base(first_commit_id, second_commit_id)
1044 1045
    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 已提交
1046
    rugged.merge_base(first_commit_id, second_commit_id)
D
Douwe Maan 已提交
1047 1048
  rescue Rugged::ReferenceError
    nil
S
Stan Hu 已提交
1049 1050
  end

1051 1052 1053 1054
  def is_ancestor?(ancestor_id, descendant_id)
    merge_base(ancestor_id, descendant_id) == ancestor_id
  end

V
Valery Sizov 已提交
1055 1056 1057 1058 1059 1060
  def empty_repo?
    !exists? || !has_visible_content?
  end

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

1062
    offset = 2
1063
    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})
1064 1065 1066
    Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
  end

V
Valery Sizov 已提交
1067 1068 1069 1070 1071 1072 1073
  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

1074
  def with_repo_branch_commit(start_repository, start_branch_name)
1075
    branch_name_or_sha =
1076 1077
      if start_repository == self
        start_branch_name
1078 1079
      else
        tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
1080

1081
        fetch_ref(
1082 1083
          start_repository.path_to_repo,
          "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
1084 1085 1086
          tmp_ref
        )

1087
        start_repository.commit(start_branch_name).sha
1088
      end
1089

1090
    yield(commit(branch_name_or_sha))
1091 1092

  ensure
1093
    rugged.references.delete(tmp_ref) if tmp_ref
1094 1095
  end

1096
  def fetch_ref(source_path, source_ref, target_ref)
1097
    args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
1098 1099 1100
    Gitlab::Popen.popen(args, path_to_repo)
  end

1101 1102 1103 1104
  def create_ref(ref, ref_path)
    fetch_ref(path_to_repo, ref, ref_path)
  end

1105 1106 1107 1108 1109
  def ls_files(ref)
    actual_ref = ref || root_ref
    raw_repository.ls_files(actual_ref)
  end

1110 1111 1112 1113
  def gitattribute(path, name)
    raw_repository.attributes(path)[name]
  end

1114 1115 1116 1117 1118 1119 1120 1121 1122 1123
  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

1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136
  # 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)
1137

1138 1139 1140 1141 1142 1143 1144 1145 1146
    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
1147 1148 1149 1150
      end
    end
  end

1151 1152 1153
  def cache_instance_variable_name(key)
    :"@#{key.to_s.tr('?!', '')}"
  end
1154

1155 1156 1157 1158
  def file_on_head(type)
    if head = tree(:head)
      head.blobs.find do |file|
        Gitlab::FileDetector.type_of(file.name) == type
1159 1160
      end
    end
1161
  end
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
  protected

  def tree_entry_at(branch_name, path)
    branch_exists?(branch_name) &&
      # tree_entry is private
      raw_repository.send(:tree_entry, commit(branch_name), path)
  end

  def check_tree_entry_for_dir(branch_name, path)
    return unless branch_exists?(branch_name)

    entry = tree_entry_at(branch_name, path)

    return unless entry

    if entry[:type] == :blob
      raise Gitlab::Git::Repository::InvalidBlobName.new(
        "Directory already exists as a file")
    else
      raise Gitlab::Git::Repository::InvalidBlobName.new(
        "Directory already exists")
    end
  end

1187 1188
  private

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

1233 1234 1235 1236
  def refs_directory_exists?
    return false unless path_with_namespace

    File.exist?(File.join(path_to_repo, 'refs'))
1237
  end
1238

1239
  def cache
1240
    @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
1241
  end
1242 1243

  def tags_sorted_by_committed_date
1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255
    tags.sort_by do |tag|
      # Annotated tags can point to any object (e.g. a blob), but generally
      # tags point to a commit. If we don't have a commit, then just default
      # to putting the tag at the end of the list.
      target = tag.dereferenced_target

      if target
        target.committed_date
      else
        Time.now
      end
    end
1256
  end
D
Douwe Maan 已提交
1257 1258 1259 1260

  def keep_around_ref_name(sha)
    "refs/keep-around/#{sha}"
  end
Y
Yorick Peterse 已提交
1261 1262 1263 1264

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