debian-mirror-gitlab/lib/gitlab/git/repository.rb

1128 lines
34 KiB
Ruby
Raw Normal View History

2019-02-15 15:39:39 +05:30
# frozen_string_literal: true
2017-08-17 22:00:37 +05:30
require 'tempfile'
require 'forwardable'
require "rubygems/package"
module Gitlab
module Git
class Repository
2018-03-17 18:26:18 +05:30
include Gitlab::Git::RepositoryMirroring
2018-12-13 13:39:08 +05:30
include Gitlab::Git::WrapsGitalyErrors
2018-05-01 15:08:00 +05:30
include Gitlab::EncodingHelper
2018-05-09 12:01:36 +05:30
include Gitlab::Utils::StrongMemoize
2019-05-03 19:53:19 +05:30
prepend Gitlab::Git::RuggedImpl::Repository
2017-08-17 22:00:37 +05:30
SEARCH_CONTEXT_LINES = 3
2018-11-18 11:00:15 +05:30
REV_LIST_COMMIT_LIMIT = 2_000
2019-12-04 20:38:33 +05:30
GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'
2018-03-17 18:26:18 +05:30
GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
2019-12-04 20:38:33 +05:30
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'
2017-08-17 22:00:37 +05:30
NoRepository = Class.new(StandardError)
2018-10-15 14:42:47 +05:30
InvalidRepository = Class.new(StandardError)
2017-08-17 22:00:37 +05:30
InvalidBlobName = Class.new(StandardError)
InvalidRef = Class.new(StandardError)
2018-03-17 18:26:18 +05:30
GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError)
TagExistsError = Class.new(StandardError)
2018-05-09 12:01:36 +05:30
ChecksumError = Class.new(StandardError)
2019-12-26 22:10:19 +05:30
class CreateTreeError < StandardError
attr_reader :error_code
def initialize(error_code)
super(self.class.name)
# The value coming from Gitaly is an uppercase String (e.g., "EMPTY")
@error_code = error_code.downcase.to_sym
end
end
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
# Directory name of repo
attr_reader :name
2018-03-17 18:26:18 +05:30
# Relative path of repo
attr_reader :relative_path
2019-03-02 22:35:43 +05:30
attr_reader :storage, :gl_repository, :relative_path, :gl_project_path
2017-08-17 22:00:37 +05:30
2019-02-15 15:39:39 +05:30
# This remote name has to be stable for all types of repositories that
# can join an object pool. If it's structure ever changes, a migration
# has to be performed on the object pools to update the remote names.
# Else the pool can't be updated anymore and is left in an inconsistent
# state.
alias_method :object_pool_remote_name, :gl_repository
2018-03-17 18:26:18 +05:30
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
2019-03-02 22:35:43 +05:30
def initialize(storage, relative_path, gl_repository, gl_project_path)
2017-08-17 22:00:37 +05:30
@storage = storage
@relative_path = relative_path
2018-03-17 18:26:18 +05:30
@gl_repository = gl_repository
2019-03-02 22:35:43 +05:30
@gl_project_path = gl_project_path
2017-08-17 22:00:37 +05:30
@name = @relative_path.split("/").last
end
2019-10-12 21:52:04 +05:30
def to_s
"<#{self.class.name}: #{self.gl_project_path}>"
end
2018-03-17 18:26:18 +05:30
def ==(other)
2019-02-15 15:39:39 +05:30
other.is_a?(self.class) && [storage, relative_path] == [other.storage, other.relative_path]
end
alias_method :eql?, :==
def hash
[self.class, storage, relative_path].hash
2018-03-17 18:26:18 +05:30
end
2017-09-10 17:25:29 +05:30
2018-11-18 11:00:15 +05:30
# This method will be removed when Gitaly reaches v1.1.
2018-10-15 14:42:47 +05:30
def path
2018-11-18 11:00:15 +05:30
File.join(
2018-10-15 14:42:47 +05:30
Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path
)
end
2017-08-17 22:00:37 +05:30
# Default branch in the repository
def root_ref
2018-11-08 19:23:39 +05:30
gitaly_ref_client.default_branch_name
rescue GRPC::NotFound => e
raise NoRepository.new(e.message)
rescue GRPC::Unknown => e
raise Gitlab::Git::CommandError.new(e.message)
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
def exists?
2018-10-15 14:42:47 +05:30
gitaly_repository_client.exists?
2018-03-17 18:26:18 +05:30
end
2019-07-31 22:56:46 +05:30
def create_repository
wrapped_gitaly_errors do
gitaly_repository_client.create_repository
end
end
2017-08-17 22:00:37 +05:30
# Returns an Array of branch names
# sorted by name ASC
def branch_names
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.branch_names
2017-08-17 22:00:37 +05:30
end
end
# Returns an Array of Branches
def branches
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.branches
2017-09-10 17:25:29 +05:30
end
2017-08-17 22:00:37 +05:30
end
# Directly find a branch with a simple name (e.g. master)
#
2018-11-18 11:00:15 +05:30
def find_branch(name)
wrapped_gitaly_errors do
gitaly_ref_client.find_branch(name)
2017-09-10 17:25:29 +05:30
end
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
def local_branches(sort_by: nil)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.local_branches(sort_by: sort_by)
2017-08-17 22:00:37 +05:30
end
end
# Returns the number of valid branches
def branch_count
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.count_branch_names
2017-08-17 22:00:37 +05:30
end
2019-12-21 20:55:43 +05:30
end
def rename(new_relative_path)
wrapped_gitaly_errors do
gitaly_repository_client.rename(new_relative_path)
end
end
def remove
wrapped_gitaly_errors do
gitaly_repository_client.remove
end
2017-08-17 22:00:37 +05:30
end
2018-05-09 12:01:36 +05:30
def expire_has_local_branches_cache
clear_memoization(:has_local_branches)
end
2018-03-17 18:26:18 +05:30
def has_local_branches?
2018-05-09 12:01:36 +05:30
strong_memoize(:has_local_branches) do
uncached_has_local_branches?
2018-03-17 18:26:18 +05:30
end
end
# 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.
alias_method :has_visible_content?, :has_local_branches?
2017-08-17 22:00:37 +05:30
# Returns the number of valid tags
def tag_count
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.count_tag_names
2017-08-17 22:00:37 +05:30
end
end
# Returns an Array of tag names
def tag_names
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.tag_names
2017-08-17 22:00:37 +05:30
end
end
# Returns an Array of Tags
2017-09-10 17:25:29 +05:30
#
2017-08-17 22:00:37 +05:30
def tags
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.tags
2017-09-10 17:25:29 +05:30
end
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
# Returns true if the given ref name exists
#
# Ref names must start with `refs/`.
def ref_exists?(ref_name)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_exists?(ref_name)
2018-03-17 18:26:18 +05:30
end
end
2017-08-17 22:00:37 +05:30
# Returns true if the given tag exists
#
# name - The name of the tag as a String.
def tag_exists?(name)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_exists?("refs/tags/#{name}")
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
# Returns true if the given branch exists
#
# name - The name of the branch as a String.
def branch_exists?(name)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_exists?("refs/heads/#{name}")
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
# Returns an Array of branch and tag names
def ref_names
branch_names + tag_names
end
2018-03-17 18:26:18 +05:30
def delete_all_refs_except(prefixes)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
2018-03-17 18:26:18 +05:30
end
end
2019-07-31 22:56:46 +05:30
def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:, path: nil)
2017-08-17 22:00:37 +05:30
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil?
2019-07-31 22:56:46 +05:30
prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha, path: path)
2017-08-17 22:00:37 +05:30
{
'ArchivePrefix' => prefix,
2018-05-09 12:01:36 +05:30
'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format),
2018-11-08 19:23:39 +05:30
'CommitId' => commit.id,
'GitalyRepository' => gitaly_repository.to_h
2017-08-17 22:00:37 +05:30
}
end
2018-05-09 12:01:36 +05:30
# This is both the filename of the archive (missing the extension) and the
# name of the top-level member of the archive under which all files go
2019-07-31 22:56:46 +05:30
def archive_prefix(ref, sha, project_path, append_sha:, path:)
2018-05-09 12:01:36 +05:30
append_sha = (ref != sha) if append_sha.nil?
formatted_ref = ref.tr('/', '-')
2018-11-08 19:23:39 +05:30
prefix_segments = [project_path, formatted_ref]
2018-05-09 12:01:36 +05:30
prefix_segments << sha if append_sha
2019-07-31 22:56:46 +05:30
prefix_segments << path.tr('/', '-').gsub(%r{^/|/$}, '') if path
2018-05-09 12:01:36 +05:30
prefix_segments.join('-')
end
private :archive_prefix
# The full path on disk where the archive should be stored. This is used
# to cache the archive between requests.
#
# The path is a global namespace, so needs to be globally unique. This is
# achieved by including `gl_repository` in the path.
#
# Archives relating to a particular ref when the SHA is not present in the
# filename must be invalidated when the ref is updated to point to a new
# SHA. This is achieved by including the SHA in the path.
#
# As this is a full path on disk, it is not "cloud native". This should
# be resolved by either removing the cache, or moving the implementation
# into Gitaly and removing the ArchivePath parameter from the git-archive
# senddata response.
def archive_file_path(storage_path, sha, name, format = "tar.gz")
2017-08-17 22:00:37 +05:30
# Build file path
2019-07-07 11:18:12 +05:30
return unless name
2017-08-17 22:00:37 +05:30
extension =
case format
when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
"tar.bz2"
when "tar"
"tar"
when "zip"
"zip"
else
# everything else should fall back to tar.gz
"tar.gz"
end
file_name = "#{name}.#{extension}"
2018-05-09 12:01:36 +05:30
File.join(storage_path, self.gl_repository, sha, file_name)
2017-08-17 22:00:37 +05:30
end
2018-05-09 12:01:36 +05:30
private :archive_file_path
2017-08-17 22:00:37 +05:30
# Return repo size in megabytes
def size
2018-11-08 19:23:39 +05:30
size = gitaly_repository_client.repository_size
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
(size.to_f / 1024).round(2)
2017-08-17 22:00:37 +05:30
end
2019-07-07 11:18:12 +05:30
# Return git object directory size in bytes
def object_directory_size
gitaly_repository_client.get_object_directory_size.to_f * 1024
end
2018-12-05 23:21:45 +05:30
# Build an array of commits.
2017-08-17 22:00:37 +05:30
#
# Usage.
# repo.log(
# ref: 'master',
# path: 'app/models',
# limit: 10,
# offset: 5,
# after: Time.new(2016, 4, 21, 14, 32, 10)
# )
def log(options)
default_options = {
limit: 10,
offset: 0,
path: nil,
follow: false,
skip_merges: false,
after: nil,
2018-03-27 19:54:05 +05:30
before: nil,
all: false
2017-08-17 22:00:37 +05:30
}
options = default_options.merge(options)
options[:offset] ||= 0
2018-03-17 18:26:18 +05:30
limit = options[:limit]
if limit == 0 || !limit.is_a?(Integer)
raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
end
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_commit_client.find_commits(options)
end
end
def new_commits(newrev)
2018-11-20 20:47:30 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.list_new_commits(newrev)
2018-03-17 18:26:18 +05:30
end
end
2019-07-07 11:18:12 +05:30
def new_blobs(newrev, dynamic_timeout: nil)
2018-11-18 11:00:15 +05:30
return [] if newrev.blank? || newrev == ::Gitlab::Git::BLANK_SHA
2018-03-17 18:26:18 +05:30
2018-11-18 11:00:15 +05:30
strong_memoize("new_blobs_#{newrev}") do
wrapped_gitaly_errors do
2019-07-07 11:18:12 +05:30
gitaly_ref_client.list_new_blobs(newrev, REV_LIST_COMMIT_LIMIT, dynamic_timeout: dynamic_timeout)
2018-11-18 11:00:15 +05:30
end
end
2017-08-17 22:00:37 +05:30
end
def count_commits(options)
2018-11-08 19:23:39 +05:30
options = process_count_commits_options(options.dup)
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
if options[:left_right]
from = options[:from]
to = options[:to]
right_count = gitaly_commit_client
.commit_count("#{from}..#{to}", options)
left_count = gitaly_commit_client
.commit_count("#{to}..#{from}", options)
[left_count, right_count]
2017-09-10 17:25:29 +05:30
else
2018-11-08 19:23:39 +05:30
gitaly_commit_client.commit_count(options[:ref], options)
2017-09-10 17:25:29 +05:30
end
end
2017-08-17 22:00:37 +05:30
end
# Counts the amount of commits between `from` and `to`.
2018-03-17 18:26:18 +05:30
def count_commits_between(from, to, options = {})
count_commits(from: from, to: to, **options)
2017-08-17 22:00:37 +05:30
end
2018-10-15 14:42:47 +05:30
# old_rev and new_rev are commit ID's
# the result of this method is an array of Gitlab::Git::RawDiffChange
def raw_changes_between(old_rev, new_rev)
@raw_changes_between ||= {}
2018-11-08 19:23:39 +05:30
@raw_changes_between[[old_rev, new_rev]] ||=
begin
return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
2018-10-15 14:42:47 +05:30
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
2018-10-15 14:42:47 +05:30
gitaly_repository_client.raw_changes_between(old_rev, new_rev)
.each_with_object([]) do |msg, arr|
msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) }
end
end
end
rescue ArgumentError => e
raise Gitlab::Git::Repository::GitError.new(e)
end
2017-08-17 22:00:37 +05:30
# Returns the SHA of the most recent common ancestor of +from+ and +to+
2018-12-13 13:39:08 +05:30
def merge_base(*commits)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
2018-12-13 13:39:08 +05:30
gitaly_repository_client.find_merge_base(*commits)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
# Returns true is +from+ is direct ancestor to +to+, otherwise false
2018-03-17 18:26:18 +05:30
def ancestor?(from, to)
2018-11-08 19:23:39 +05:30
gitaly_commit_client.ancestor?(from, to)
2018-03-17 18:26:18 +05:30
end
def merged_branch_names(branch_names = [])
return [] unless root_ref
root_sha = find_branch(root_ref)&.target
return [] unless root_sha
2018-11-18 11:00:15 +05:30
branches = wrapped_gitaly_errors do
gitaly_merged_branch_names(branch_names, root_sha)
2018-03-17 18:26:18 +05:30
end
Set.new(branches)
2017-08-17 22:00:37 +05:30
end
# Return an array of Diff objects that represent the diff
# between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to
# split larger rewrites into delete/add pairs.
def diff(from, to, options = {}, *paths)
2018-11-18 11:00:15 +05:30
iterator = gitaly_commit_client.diff(from, to, options.merge(paths: paths))
2018-03-17 18:26:18 +05:30
Gitlab::Git::DiffCollection.new(iterator, options)
2017-08-17 22:00:37 +05:30
end
2018-12-05 23:21:45 +05:30
def diff_stats(left_id, right_id)
2019-02-15 15:39:39 +05:30
if [left_id, right_id].any? { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) }
return empty_diff_stats
end
2018-12-05 23:21:45 +05:30
stats = wrapped_gitaly_errors do
gitaly_commit_client.diff_stats(left_id, right_id)
end
Gitlab::Git::DiffStatsCollection.new(stats)
rescue CommandError, TypeError
2019-02-15 15:39:39 +05:30
empty_diff_stats
2018-12-05 23:21:45 +05:30
end
2017-08-17 22:00:37 +05:30
# Returns a RefName for a given SHA
def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present?
2018-11-08 19:23:39 +05:30
gitaly_ref_client.find_ref_name(sha, ref_path)
2017-08-17 22:00:37 +05:30
end
2019-02-15 15:39:39 +05:30
# Get refs hash which key is the commit id
2018-03-17 18:26:18 +05:30
# and value is a Gitlab::Git::Tag or Gitlab::Git::Branch
# Note that both inherit from Gitlab::Git::Ref
def refs_hash
return @refs_hash if @refs_hash
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
@refs_hash = Hash.new { |h, k| h[k] = [] }
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
(tags + branches).each do |ref|
2019-07-07 11:18:12 +05:30
next unless ref.target && ref.name && ref.dereferenced_target&.id
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
@refs_hash[ref.dereferenced_target.id] << ref.name
2017-08-17 22:00:37 +05:30
end
@refs_hash
end
2017-09-10 17:25:29 +05:30
# Returns url for submodule
2017-08-17 22:00:37 +05:30
#
# Ex.
2017-09-10 17:25:29 +05:30
# @repository.submodule_url_for('master', 'rack')
# # => git@localhost:rack.git
2017-08-17 22:00:37 +05:30
#
2017-09-10 17:25:29 +05:30
def submodule_url_for(ref, path)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_submodule_url_for(ref, path)
2017-08-17 22:00:37 +05:30
end
end
2019-09-30 21:07:59 +05:30
# Returns path to url mappings for submodules
#
# Ex.
# @repository.submodule_urls_for('master')
# # => { 'rack' => 'git@localhost:rack.git' }
#
def submodule_urls_for(ref)
wrapped_gitaly_errors do
gitaly_submodule_urls_for(ref)
end
end
2017-08-17 22:00:37 +05:30
# Return total commits count accessible from passed ref
def commit_count(ref)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_commit_client.commit_count(ref)
2017-09-10 17:25:29 +05:30
end
2017-08-17 22:00:37 +05:30
end
2019-07-07 11:18:12 +05:30
# Return total diverging commits count
2019-09-04 21:01:54 +05:30
def diverging_commit_count(from, to, max_count: 0)
2019-07-07 11:18:12 +05:30
wrapped_gitaly_errors do
gitaly_commit_client.diverging_commit_count(from, to, max_count: max_count)
end
end
2017-08-17 22:00:37 +05:30
# Mimic the `git clean` command and recursively delete untracked files.
# Valid keys that can be passed in the +options+ hash are:
#
# :d - Remove untracked directories
# :f - Remove untracked directories that are managed by a different
# repository
# :x - Remove ignored files
#
# The value in +options+ must evaluate to true for an option to take
# effect.
#
# Examples:
#
# repo.clean(d: true, f: true) # Enable the -d and -f options
#
# repo.clean(d: false, x: true) # -x is enabled, -d is not
def clean(options = {})
strategies = [:remove_untracked]
strategies.push(:force) if options[:f]
strategies.push(:remove_ignored) if options[:x]
# TODO: implement this method
end
2018-03-17 18:26:18 +05:30
def add_branch(branch_name, user:, target:)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_create_branch(branch_name, user, target)
2018-03-17 18:26:18 +05:30
end
end
def add_tag(tag_name, user:, target:, message: nil)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.add_tag(tag_name, user, target, message)
2018-03-17 18:26:18 +05:30
end
end
2018-11-08 19:23:39 +05:30
def update_branch(branch_name, user:, newrev:, oldrev:)
2018-11-20 20:47:30 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
2018-11-18 11:00:15 +05:30
end
2018-11-08 19:23:39 +05:30
end
2018-03-17 18:26:18 +05:30
def rm_branch(branch_name, user:)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_delete_branch(branch_name, user)
2018-03-17 18:26:18 +05:30
end
end
def rm_tag(tag_name, user:)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.rm_tag(tag_name, user)
2018-03-17 18:26:18 +05:30
end
end
def find_tag(name)
tags.find { |tag| tag.name == name }
end
2019-09-30 21:07:59 +05:30
def merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
2019-07-07 11:18:12 +05:30
wrapped_gitaly_errors do
2019-09-30 21:07:59 +05:30
gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
2019-07-07 11:18:12 +05:30
end
end
2018-03-17 18:26:18 +05:30
def merge(user, source_sha, target_branch, message, &block)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block)
2018-03-17 18:26:18 +05:30
end
end
def ff_merge(user, source_sha, target_branch)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_ff_branch(user, source_sha, target_branch)
2018-03-17 18:26:18 +05:30
end
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
2018-11-08 19:23:39 +05:30
args = {
user: user,
commit: commit,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_repository
}
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_revert(args)
2018-03-17 18:26:18 +05:30
end
end
def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
2018-11-08 19:23:39 +05:30
args = {
user: user,
commit: commit,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_repository
}
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_cherry_pick(args)
2018-03-17 18:26:18 +05:30
end
end
2018-12-13 13:39:08 +05:30
def update_submodule(user:, submodule:, commit_sha:, message:, branch:)
args = {
user: user,
submodule: submodule,
commit_sha: commit_sha,
branch: branch,
message: message
}
wrapped_gitaly_errors do
gitaly_operation_client.user_update_submodule(args)
end
end
2017-08-17 22:00:37 +05:30
# Delete the specified branch from the repository
def delete_branch(branch_name)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.delete_branch(branch_name)
2018-03-17 18:26:18 +05:30
end
2018-11-18 11:00:15 +05:30
rescue CommandError => e
2018-03-17 18:26:18 +05:30
raise DeleteBranchError, e
end
def delete_refs(*ref_names)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_delete_refs(*ref_names)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
# Create a new branch named **ref+ based on **stat_point+, HEAD by default
#
# Examples:
# create_branch("feature")
# create_branch("other-feature", "master")
def create_branch(ref, start_point = "HEAD")
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_ref_client.create_branch(ref, start_point)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
# If `mirror_refmap` is present the remote is set as mirror with that mapping
def add_remote(remote_name, url, mirror_refmap: nil)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_remote_client.add_remote(remote_name, url, mirror_refmap)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
def remove_remote(remote_name)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_remote_client.remove_remote(remote_name)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
2018-11-20 20:47:30 +05:30
def find_remote_root_ref(remote_name)
return unless remote_name.present?
wrapped_gitaly_errors do
gitaly_remote_client.find_remote_root_ref(remote_name)
end
end
2017-08-17 22:00:37 +05:30
# Returns result like "git ls-files" , recursive and full file path
#
# Ex.
# repo.ls_files('master')
#
def ls_files(ref)
2018-11-08 19:23:39 +05:30
gitaly_commit_client.ls_files(ref)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
def copy_gitattributes(ref)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.apply_gitattributes(ref)
2017-08-17 22:00:37 +05:30
end
end
2018-10-15 14:42:47 +05:30
def info_attributes
return @info_attributes if @info_attributes
2018-11-08 19:23:39 +05:30
content = gitaly_repository_client.info_attributes
2018-10-15 14:42:47 +05:30
@info_attributes = AttributesParser.new(content)
end
2017-08-17 22:00:37 +05:30
# Returns the Git attributes for the given file path.
#
# See `Gitlab::Git::Attributes` for more information.
def attributes(path)
2018-10-15 14:42:47 +05:30
info_attributes.attributes(path)
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
def gitattribute(path, name)
attributes(path)[name]
end
2019-09-30 21:07:59 +05:30
# Returns parsed .gitattributes for a given ref
2018-03-17 18:26:18 +05:30
#
2019-09-30 21:07:59 +05:30
# This only parses the root .gitattributes file,
2018-03-17 18:26:18 +05:30
# it does not traverse subfolders to find additional .gitattributes files
#
2018-05-09 12:01:36 +05:30
# This method is around 30 times slower than `attributes`, which uses
# `$GIT_DIR/info/attributes`. Consider caching AttributesAtRefParser
# and reusing that for multiple calls instead of this method.
2019-09-30 21:07:59 +05:30
def attributes_at(ref)
AttributesAtRefParser.new(self, ref)
2018-03-17 18:26:18 +05:30
end
2017-09-10 17:25:29 +05:30
def languages(ref = nil)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_commit_client.languages(ref)
2017-09-10 17:25:29 +05:30
end
end
2018-03-27 19:54:05 +05:30
def license_short_name
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.license_short_name
2018-03-27 19:54:05 +05:30
end
end
2018-03-17 18:26:18 +05:30
def fetch_source_branch!(source_repository, source_branch, local_ref)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref)
2018-03-17 18:26:18 +05:30
end
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
2019-07-31 22:56:46 +05:30
reachable_ref =
if source_repository == self
source_branch_name
else
# If a tmp ref was created before for a separate repo comparison (forks),
# we're able to short-circuit the tmp ref re-creation:
# 1. Take the SHA from the source repo
# 2. Read that in the current "target" repo
# 3. If that SHA is still known (readable), it means GC hasn't
# cleaned it up yet, so we can use it instead re-writing the tmp ref.
source_commit_id = source_repository.commit(source_branch_name)&.sha
commit(source_commit_id)&.sha if source_commit_id
end
return compare(target_branch_name, reachable_ref, straight: straight) if reachable_ref
2018-11-08 19:23:39 +05:30
tmp_ref = "refs/tmp/#{SecureRandom.hex}"
return unless fetch_source_branch!(source_repository, source_branch_name, tmp_ref)
2019-07-31 22:56:46 +05:30
compare(target_branch_name, tmp_ref, straight: straight)
2018-11-08 19:23:39 +05:30
ensure
2019-07-31 22:56:46 +05:30
delete_refs(tmp_ref) if tmp_ref
2018-03-17 18:26:18 +05:30
end
2019-02-15 15:39:39 +05:30
def write_ref(ref_path, ref, old_ref: nil)
2018-03-17 18:26:18 +05:30
ref_path = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref_path}" unless ref_path.start_with?("refs/") || ref_path == "HEAD"
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
2019-02-15 15:39:39 +05:30
gitaly_repository_client.write_ref(ref_path, ref, old_ref)
2018-03-17 18:26:18 +05:30
end
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
def commit(ref = 'HEAD')
Gitlab::Git::Commit.find(self, ref)
end
def empty?
!has_visible_content?
end
def fetch_repository_as_mirror(repository)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_remote_client.fetch_internal_remote(repository)
2018-03-17 18:26:18 +05:30
end
end
2018-12-13 13:39:08 +05:30
# Fetch remote for repository
#
# remote - remote name
# ssh_auth - SSH known_hosts data and a private key to use for public-key authentication
# forced - should we use --force flag?
# no_tags - should we use --no-tags flag?
# prune - should we use --prune flag?
def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
wrapped_gitaly_errors do
gitaly_repository_client.fetch_remote(
remote,
ssh_auth: ssh_auth,
forced: forced,
no_tags: no_tags,
prune: prune,
timeout: GITLAB_PROJECTS_TIMEOUT
)
end
end
2018-03-17 18:26:18 +05:30
def blob_at(sha, path)
Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha)
end
# Items should be of format [[commit_id, path], [commit_id1, path1]]
def batch_blobs(items, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
end
def fsck
2018-05-09 12:01:36 +05:30
msg, status = gitaly_repository_client.fsck
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
raise GitError.new("Could not fsck repository: #{msg}") unless status.zero?
2018-03-17 18:26:18 +05:30
end
def create_from_bundle(bundle_path)
2019-01-20 21:35:32 +05:30
# It's important to check that the linked-to file is actually a valid
# .bundle file as it is passed to `git clone`, which may otherwise
# interpret it as a pointer to another repository
::Gitlab::Git::BundleFile.check!(bundle_path)
2018-11-08 19:23:39 +05:30
gitaly_repository_client.create_from_bundle(bundle_path)
2018-03-17 18:26:18 +05:30
end
2018-05-09 12:01:36 +05:30
def create_from_snapshot(url, auth)
gitaly_repository_client.create_from_snapshot(url, auth)
end
2019-07-31 22:56:46 +05:30
# DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628
def rebase_deprecated(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_rebase(user, rebase_id,
branch: branch,
branch_sha: branch_sha,
remote_repository: remote_repository,
remote_branch: remote_branch)
2018-03-17 18:26:18 +05:30
end
end
2019-07-31 22:56:46 +05:30
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, &block)
wrapped_gitaly_errors do
gitaly_operation_client.rebase(
user,
rebase_id,
branch: branch,
branch_sha: branch_sha,
remote_repository: remote_repository,
remote_branch: remote_branch,
&block
)
end
end
2018-03-17 18:26:18 +05:30
def rebase_in_progress?(rebase_id)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.rebase_in_progress?(rebase_id)
2018-03-17 18:26:18 +05:30
end
end
def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_squash(user, squash_id, branch,
2018-03-17 18:26:18 +05:30
start_sha, end_sha, author, message)
end
end
def squash_in_progress?(squash_id)
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.squash_in_progress?(squash_id)
2018-03-27 19:54:05 +05:30
end
2018-03-17 18:26:18 +05:30
end
def bundle_to_disk(save_path)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.create_bundle(save_path)
2018-03-17 18:26:18 +05:30
end
true
end
2019-07-07 11:18:12 +05:30
# rubocop:disable Metrics/ParameterLists
2018-03-17 18:26:18 +05:30
def multi_action(
user, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
2019-10-12 21:52:04 +05:30
start_branch_name: nil, start_sha: nil, start_repository: self,
2019-07-07 11:18:12 +05:30
force: false)
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_operation_client.user_commit_files(user, branch_name,
2018-03-17 18:26:18 +05:30
message, actions, author_email, author_name,
2019-10-12 21:52:04 +05:30
start_branch_name, start_repository, force, start_sha)
2018-03-17 18:26:18 +05:30
end
end
2019-07-07 11:18:12 +05:30
# rubocop:enable Metrics/ParameterLists
2018-03-17 18:26:18 +05:30
def write_config(full_path:)
return unless full_path.present?
2018-11-08 19:23:39 +05:30
# This guard avoids Gitaly log/error spam
raise NoRepository, 'repository does not exist' unless exists?
set_config('gitlab.fullpath' => full_path)
end
def set_config(entries)
wrapped_gitaly_errors do
gitaly_repository_client.set_config(entries)
2018-03-17 18:26:18 +05:30
end
end
2018-11-08 19:23:39 +05:30
def delete_config(*keys)
wrapped_gitaly_errors do
gitaly_repository_client.delete_config(keys)
end
2018-03-17 18:26:18 +05:30
end
2019-07-31 22:56:46 +05:30
def disconnect_alternates
wrapped_gitaly_errors do
gitaly_repository_client.disconnect_alternates
end
end
2018-11-08 19:23:39 +05:30
def gitaly_repository
2019-03-02 22:35:43 +05:30
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository, @gl_project_path)
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::RefService.new(self)
end
def gitaly_commit_client
@gitaly_commit_client ||= Gitlab::GitalyClient::CommitService.new(self)
end
def gitaly_repository_client
@gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self)
end
2018-03-17 18:26:18 +05:30
def gitaly_operation_client
@gitaly_operation_client ||= Gitlab::GitalyClient::OperationService.new(self)
end
def gitaly_remote_client
@gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self)
end
def gitaly_blob_client
@gitaly_blob_client ||= Gitlab::GitalyClient::BlobService.new(self)
end
def gitaly_conflicts_client(our_commit_oid, their_commit_oid)
Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid)
end
2018-05-09 12:01:36 +05:30
def clean_stale_repository_files
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.cleanup if exists?
2018-05-09 12:01:36 +05:30
end
rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup
2019-09-30 21:07:59 +05:30
Rails.logger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}") # rubocop:disable Gitlab/RailsLogger
2018-05-09 12:01:36 +05:30
Gitlab::Metrics.counter(
:failed_repository_cleanup_total,
'Number of failed repository cleanup events'
).increment
end
2018-03-17 18:26:18 +05:30
def branch_names_contains_sha(sha)
2018-11-08 19:23:39 +05:30
gitaly_ref_client.branch_names_contains_sha(sha)
2018-03-17 18:26:18 +05:30
end
def tag_names_contains_sha(sha)
2018-11-08 19:23:39 +05:30
gitaly_ref_client.tag_names_contains_sha(sha)
2018-03-17 18:26:18 +05:30
end
def search_files_by_content(query, ref)
return [] if empty? || query.blank?
2018-11-08 19:23:39 +05:30
safe_query = Regexp.escape(query)
ref ||= root_ref
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
gitaly_repository_client.search_files_by_content(ref, safe_query)
2018-03-17 18:26:18 +05:30
end
def can_be_merged?(source_sha, target_branch)
2018-11-18 11:00:15 +05:30
if target_sha = find_branch(target_branch)&.target
2018-11-08 19:23:39 +05:30
!gitaly_conflicts_client(source_sha, target_sha).conflicts?
else
false
2018-03-17 18:26:18 +05:30
end
end
def search_files_by_name(query, ref)
safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
2018-11-08 19:23:39 +05:30
ref ||= root_ref
2018-03-17 18:26:18 +05:30
return [] if empty? || safe_query.blank?
2018-11-08 19:23:39 +05:30
gitaly_repository_client.search_files_by_name(ref, safe_query)
2018-03-17 18:26:18 +05:30
end
def find_commits_by_message(query, ref, path, limit, offset)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_commit_client
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) }
2018-03-17 18:26:18 +05:30
end
end
2018-12-05 23:21:45 +05:30
def list_last_commits_for_tree(sha, path, offset: 0, limit: 25)
wrapped_gitaly_errors do
gitaly_commit_client.list_last_commits_for_tree(sha, path, offset: offset, limit: limit)
end
2018-03-17 18:26:18 +05:30
end
2018-03-27 19:54:05 +05:30
def last_commit_for_path(sha, path)
2018-11-18 11:00:15 +05:30
wrapped_gitaly_errors do
gitaly_commit_client.last_commit_for_path(sha, path)
2018-03-17 18:26:18 +05:30
end
end
2018-05-09 12:01:36 +05:30
def checksum
2018-11-08 19:23:39 +05:30
# The exists? RPC is much cheaper, so we perform this request first
raise NoRepository, "Repository does not exists" unless exists?
gitaly_repository_client.calculate_checksum
rescue GRPC::NotFound
raise NoRepository # Guard against data races.
2018-05-09 12:01:36 +05:30
end
2017-08-17 22:00:37 +05:30
private
2019-07-31 22:56:46 +05:30
def compare(base_ref, head_ref, straight:)
Gitlab::Git::Compare.new(self,
base_ref,
head_ref,
straight: straight)
end
2019-02-15 15:39:39 +05:30
def empty_diff_stats
Gitlab::Git::DiffStatsCollection.new([])
end
2018-05-09 12:01:36 +05:30
def uncached_has_local_branches?
2018-11-08 19:23:39 +05:30
wrapped_gitaly_errors do
gitaly_repository_client.has_local_branches?
2018-05-09 12:01:36 +05:30
end
end
2018-03-17 18:26:18 +05:30
def gitaly_merged_branch_names(branch_names, root_sha)
qualified_branch_names = branch_names.map { |b| "refs/heads/#{b}" }
gitaly_ref_client.merged_branches(qualified_branch_names)
.reject { |b| b.target == root_sha }
.map(&:name)
2017-09-10 17:25:29 +05:30
end
2018-03-17 18:26:18 +05:30
def process_count_commits_options(options)
if options[:from] || options[:to]
ref =
if options[:left_right] # Compare with merge-base for left-right
"#{options[:from]}...#{options[:to]}"
else
"#{options[:from]}..#{options[:to]}"
end
options.merge(ref: ref)
elsif options[:ref] && options[:left_right]
from, to = options[:ref].match(/\A([^\.]*)\.{2,3}([^\.]*)\z/)[1..2]
options.merge(from: from, to: to)
else
options
end
2017-09-10 17:25:29 +05:30
end
def gitaly_submodule_url_for(ref, path)
# We don't care about the contents so 1 byte is enough. Can't request 0 bytes, 0 means unlimited.
commit_object = gitaly_commit_client.tree_entry(ref, path, 1)
return unless commit_object && commit_object.type == :COMMIT
2019-09-30 21:07:59 +05:30
urls = gitaly_submodule_urls_for(ref)
urls && urls[path]
end
def gitaly_submodule_urls_for(ref)
2017-09-10 17:25:29 +05:30
gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
return unless gitmodules
2019-09-30 21:07:59 +05:30
submodules = GitmodulesParser.new(gitmodules.data).parse
submodules.transform_values { |submodule| submodule['url'] }
2017-09-10 17:25:29 +05:30
end
2018-03-17 18:26:18 +05:30
# Returns true if the given ref name exists
#
# Ref names must start with `refs/`.
def gitaly_ref_exists?(ref_name)
gitaly_ref_client.ref_exists?(ref_name)
end
def gitaly_copy_gitattributes(revision)
gitaly_repository_client.apply_gitattributes(revision)
end
def gitaly_delete_refs(*ref_names)
2018-06-03 19:52:53 +05:30
gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
end
end