gitlab-org--gitlab-foss/app/models/repository.rb

1247 lines
32 KiB
Ruby
Raw Normal View History

require 'securerandom'
class Repository
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
CommitError = Class.new(StandardError)
# 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}"
alias_method(original, name)
define_method(name) do
cache_method_output(name, fallback: fallback) { __send__(original) }
end
end
def self.storages
Gitlab.config.repositories.storages
end
def initialize(path_with_namespace, project)
2013-10-01 14:00:28 +00:00
@path_with_namespace = path_with_namespace
@project = project
end
def raw_repository
return nil unless path_with_namespace
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
end
# Return absolute path to repository
2013-10-01 14:00:28 +00:00
def path_to_repo
@path_to_repo ||= File.expand_path(
File.join(@project.repository_storage_path, path_with_namespace + ".git")
)
2013-10-01 14:00:28 +00:00
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.
#
# This method return true if repository contains some content visible in project page.
#
def has_visible_content?
branch_count > 0
end
2016-07-07 11:33:54 +00:00
def commit(ref = 'HEAD')
return nil unless exists?
commit =
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
commit = ::Commit.new(commit, @project) if commit
commit
rescue Rugged::OdbError, Rugged::TreeError
nil
end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
options = {
2013-08-05 13:51:04 +00:00
repo: raw_repository,
ref: ref,
path: path,
limit: limit,
offset: offset,
after: after,
before: before,
Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll `git` doesn't work properly when `--follow` and `--skip` are specified together. We could even be **omitting commits in the Web log** as a result. Here are the gory details. Let's say you ran: ``` git log -n=5 --skip=2 README ``` This is the working case since it omits `--follow`. This is what happens: 1. `git` starts at `HEAD` and traverses down the tree until it finds the top-most commit relevant to README. 2. Once this is found, this commit is returned via `get_revision_1()`. 3. If the `skip_count` is positive, decrement and repeat step 2. Otherwise go onto step 4. 4. `show_log()` gets called with that commit. 5. Repeat step 1 until we have all five entries. That's exactly what we want. What happens when you use `--follow`? You have to understand how step 1 is performed: * When you specify a pathspec on the command-line (e.g. README), a flag `prune` [gets set here](https://github.com/git/git/blob/master/revision.c#L2351). * If the `prune` flag is active, `get_commit_action()` determines whether the commit should be [scanned for matching paths](https://github.com/git/git/blob/master/revision.c#L2989). * In the case of `--follow`, however, `prune` is [disabled here](https://github.com/git/git/blob/master/revision.c#L2350). * As a result, a commit is never scanned for matching paths and therefore never pruned. `HEAD` will always get returned as the first commit, even if it's not relevant to the README. * Making matters worse, the `--skip` in the example above would actually skip every other after `HEAD` N times. If README were changed in these skipped commits, we would actually miss information! Since git uses a matching algorithm to determine whether a file was renamed, I believe `git` needs to generate a diff of each commit to do this and traverse each commit one-by-one to do this. I think that's the rationale for disabling the `prune` functionality since you can't just do a simple string comparison. Closes #4181, #4229, #3574, #2410
2015-12-25 09:07:00 +00:00
# --follow doesn't play well with --skip. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
follow: false,
skip_merges: skip_merges
}
commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
commits
end
2013-08-05 13:51:04 +00:00
def commits_between(from, to)
commits = Gitlab::Git::Commit.between(raw_repository, from, to)
commits = Commit.decorate(commits, @project) if commits.present?
commits
end
def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
2016-10-19 16:43:04 +00:00
unless exists? && has_visible_content? && query.present?
return []
end
ref ||= root_ref
args = %W(
#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset}
--max-count #{limit} --grep=#{query} --regexp-ignore-case
)
args = args.concat(%W(-- #{path})) if path.present?
2016-10-19 16:43:04 +00:00
git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
git_log_results.map { |c| commit(c.chomp) }.compact
end
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)
end
def find_tag(name)
tags.find { |tag| tag.name == name }
end
def add_branch(user, branch_name, ref)
newrev = commit(ref).try(:sha)
return false unless newrev
GitOperationService.new(user, self).add_branch(branch_name, newrev)
2013-07-16 21:09:23 +00:00
after_create_branch
find_branch(branch_name)
2013-07-16 21:09:23 +00:00
end
def add_tag(user, tag_name, target, message = nil)
newrev = commit(target).try(:id)
options = { message: message, tagger: user_to_committer(user) } if message
return false unless newrev
GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
2013-07-17 11:43:18 +00:00
find_tag(tag_name)
2013-07-17 11:43:18 +00:00
end
def rm_branch(user, branch_name)
before_remove_branch
branch = find_branch(branch_name)
GitOperationService.new(user, self).rm_branch(branch)
2013-07-16 21:09:23 +00:00
after_remove_branch
true
end
# TODO: why we don't pass user here?
def rm_tag(tag_name)
before_remove_tag
2013-07-16 21:09:23 +00:00
2016-04-15 12:21:28 +00:00
begin
rugged.tags.delete(tag_name)
true
rescue Rugged::ReferenceError
false
end
end
def ref_names
branch_names + tag_names
end
2016-05-09 22:45:37 +00:00
def branch_exists?(branch_name)
branch_names.include?(branch_name)
end
def ref_exists?(ref)
rugged.references.exist?(ref)
rescue Rugged::ReferenceError
false
end
2016-07-04 00:30:55 +00:00
# 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.
def keep_around(sha)
return unless sha && commit(sha)
return if kept_around?(sha)
# 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}"
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}"
end
end
def kept_around?(sha)
ref_exists?(keep_around_ref_name(sha))
end
def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
2015-11-11 22:29:29 +00:00
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
number_commits_behind = raw_repository.
count_commits_between(branch.dereferenced_target.sha, root_ref_hash)
number_commits_ahead = raw_repository.
count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
{ behind: number_commits_behind, ahead: number_commits_ahead }
end
end
2013-04-28 20:04:56 +00:00
def expire_tags_cache
expire_method_caches(%i(tag_names tag_count))
@tags = nil
end
def expire_branches_cache
expire_method_caches(%i(branch_names branch_count))
@local_branches = nil
end
def expire_statistics_caches
expire_method_caches(%i(size commit_count))
end
def expire_all_method_caches
expire_method_caches(CACHED_METHODS)
2015-11-11 15:28:31 +00:00
end
# 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
2015-11-11 15:28:31 +00:00
end
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
end
expire_method_caches(to_refresh)
to_refresh.each { |method| send(method) }
end
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}")
end
2013-06-25 10:55:03 +00:00
end
def expire_root_ref_cache
expire_method_caches(%i(root_ref))
end
# Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches
return unless empty?
expire_method_caches(%i(empty?))
end
def lookup_cache
@lookup_cache ||= {}
end
def expire_exists_cache
expire_method_caches(%i(exists?))
end
# 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
expire_statistics_caches
end
# Runs code after a repository has been created.
def after_create
expire_exists_cache
expire_root_ref_cache
expire_emptiness_caches
repository_event(:create_repository)
end
# Runs code just before a repository is deleted.
def before_delete
expire_exists_cache
expire_all_method_caches
expire_branch_cache if exists?
expire_content_cache
repository_event(:remove_repository)
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
repository_event(:change_default_branch)
end
# Runs code before pushing (= creating or removing) a tag.
def before_push_tag
expire_statistics_caches
expire_emptiness_caches
expire_tags_cache
repository_event(:push_tag)
end
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
expire_statistics_caches
repository_event(:remove_tag)
end
def before_import
expire_content_cache
end
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
expire_tags_cache
expire_branches_cache
end
# Runs code after a new commit has been pushed.
def after_push_commit(branch_name)
expire_statistics_caches
expire_branch_cache(branch_name)
repository_event(:push_commit, branch: branch_name)
end
# Runs code after a new branch has been created.
def after_create_branch
expire_branches_cache
repository_event(:push_branch)
end
# Runs code before removing an existing branch.
def before_remove_branch
expire_branches_cache
repository_event(:remove_branch)
end
# Runs code after an existing branch has been removed.
def after_remove_branch
expire_branches_cache
end
def method_missing(m, *args, &block)
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
end
def respond_to_missing?(method, include_private = false)
raw_repository.respond_to?(method, include_private) || super
end
2013-10-01 17:34:41 +00:00
def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
2013-10-01 17:34:41 +00:00
end
def blob_by_oid(oid)
Gitlab::Git::Blob.raw(self, oid)
end
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
def readme
if head = tree(:head)
head.readme
end
end
cache_method :readme
def version
file_on_head(:version)
end
cache_method :version
def contribution_guide
file_on_head(:contributing)
end
cache_method :contribution_guide
def changelog
file_on_head(:changelog)
end
cache_method :changelog
def license_blob
file_on_head(:license)
end
cache_method :license_blob
2015-10-01 18:34:23 +00:00
def license_key
return unless exists?
Licensee.license(path).try(:key)
end
cache_method :license_key
2016-04-29 14:25:03 +00:00
def gitignore
file_on_head(:gitignore)
2016-04-29 14:25:03 +00:00
end
cache_method :gitignore
2016-04-29 14:25:03 +00:00
Support integration with Koding (online IDE) Koding: #index: landing page for Koding integration If enabled it will provide a link to open remote Koding instance url for now we are also providing the sneak preview video for how integration works in detail. Repository: check whether .koding.yml file exists on repository Projects: landing page: show Run in IDE (Koding) button if repo has stack file Projects: MR: show Run in IDE Koding button if repo has stack file on active branch ProjectHelpers: add_koding_stack: stack generator for provided project With this helper we will auto-generate the required stack template for a given project. For the feature we can request this base template from the running Koding instance on integration. Currently this will provide users to create a t2.nano instance on aws and it'll automatically configures the instance for basic requirements. Projects: empty state and landing page provide shortcuts to create stack projects_helper: use branch on checkout and provide an entry point This ${var.koding_queryString_branch} will be replaced with the branch provided in query string which will allow us to use same stack template for different branches of the same repository. ref: https://github.com/koding/koding/pull/8597/commits/b8c0e43c4c24bf132670aa8a3cfb0d634acfd09b projects_helper: provide sha info in query string to use existing vms With this change we'll be able to query existing vms on Koding side based on the commit id that they've created. ref: https://github.com/koding/koding/pull/8597/commits/1d630fadf31963fa6ccd3bed92e526761a30a343 Integration: Docs: Koding documentation added Disable /koding route if integration is disabled Use application settings to enable Koding Projects_helper: better indentation with strip_heredoc usage Projects_helper: return koding_url as is if there is no project provided current_settings: set koding_enabled: false by default Koding_Controller: to render not_found once integration is disabled Dashboard_specs: update spec for Koding enabled case Projects_Helper: make repo dynamic ref: https://github.com/koding/koding/pull/8597/commits/4d615242f45aaea4c4986be84ecc612b0bb1514c Updated documentation to have right format
2016-07-26 03:59:39 +00:00
def koding_yml
file_on_head(:koding)
Support integration with Koding (online IDE) Koding: #index: landing page for Koding integration If enabled it will provide a link to open remote Koding instance url for now we are also providing the sneak preview video for how integration works in detail. Repository: check whether .koding.yml file exists on repository Projects: landing page: show Run in IDE (Koding) button if repo has stack file Projects: MR: show Run in IDE Koding button if repo has stack file on active branch ProjectHelpers: add_koding_stack: stack generator for provided project With this helper we will auto-generate the required stack template for a given project. For the feature we can request this base template from the running Koding instance on integration. Currently this will provide users to create a t2.nano instance on aws and it'll automatically configures the instance for basic requirements. Projects: empty state and landing page provide shortcuts to create stack projects_helper: use branch on checkout and provide an entry point This ${var.koding_queryString_branch} will be replaced with the branch provided in query string which will allow us to use same stack template for different branches of the same repository. ref: https://github.com/koding/koding/pull/8597/commits/b8c0e43c4c24bf132670aa8a3cfb0d634acfd09b projects_helper: provide sha info in query string to use existing vms With this change we'll be able to query existing vms on Koding side based on the commit id that they've created. ref: https://github.com/koding/koding/pull/8597/commits/1d630fadf31963fa6ccd3bed92e526761a30a343 Integration: Docs: Koding documentation added Disable /koding route if integration is disabled Use application settings to enable Koding Projects_helper: better indentation with strip_heredoc usage Projects_helper: return koding_url as is if there is no project provided current_settings: set koding_enabled: false by default Koding_Controller: to render not_found once integration is disabled Dashboard_specs: update spec for Koding enabled case Projects_Helper: make repo dynamic ref: https://github.com/koding/koding/pull/8597/commits/4d615242f45aaea4c4986be84ecc612b0bb1514c Updated documentation to have right format
2016-07-26 03:59:39 +00:00
end
cache_method :koding_yml
Support integration with Koding (online IDE) Koding: #index: landing page for Koding integration If enabled it will provide a link to open remote Koding instance url for now we are also providing the sneak preview video for how integration works in detail. Repository: check whether .koding.yml file exists on repository Projects: landing page: show Run in IDE (Koding) button if repo has stack file Projects: MR: show Run in IDE Koding button if repo has stack file on active branch ProjectHelpers: add_koding_stack: stack generator for provided project With this helper we will auto-generate the required stack template for a given project. For the feature we can request this base template from the running Koding instance on integration. Currently this will provide users to create a t2.nano instance on aws and it'll automatically configures the instance for basic requirements. Projects: empty state and landing page provide shortcuts to create stack projects_helper: use branch on checkout and provide an entry point This ${var.koding_queryString_branch} will be replaced with the branch provided in query string which will allow us to use same stack template for different branches of the same repository. ref: https://github.com/koding/koding/pull/8597/commits/b8c0e43c4c24bf132670aa8a3cfb0d634acfd09b projects_helper: provide sha info in query string to use existing vms With this change we'll be able to query existing vms on Koding side based on the commit id that they've created. ref: https://github.com/koding/koding/pull/8597/commits/1d630fadf31963fa6ccd3bed92e526761a30a343 Integration: Docs: Koding documentation added Disable /koding route if integration is disabled Use application settings to enable Koding Projects_helper: better indentation with strip_heredoc usage Projects_helper: return koding_url as is if there is no project provided current_settings: set koding_enabled: false by default Koding_Controller: to render not_found once integration is disabled Dashboard_specs: update spec for Koding enabled case Projects_Helper: make repo dynamic ref: https://github.com/koding/koding/pull/8597/commits/4d615242f45aaea4c4986be84ecc612b0bb1514c Updated documentation to have right format
2016-07-26 03:59:39 +00:00
def gitlab_ci_yml
file_on_head(:gitlab_ci)
end
cache_method :gitlab_ci_yml
def head_commit
2015-03-21 20:45:08 +00:00
@head_commit ||= commit(self.root_ref)
end
def head_tree
if head_commit
@head_tree ||= Tree.new(self, head_commit.sha, nil)
end
end
def tree(sha = :head, path = nil, recursive: false)
if sha == :head
return unless head_commit
2015-03-21 20:45:08 +00:00
if path.nil?
return head_tree
else
sha = head_commit.sha
end
end
Tree.new(self, sha, path, recursive: recursive)
end
def blob_at_branch(branch_name, path)
last_commit = commit(branch_name)
if last_commit
blob_at(last_commit.sha, path)
else
nil
end
end
# Returns url for submodule
#
# Ex.
# @repository.submodule_url_for('master', 'rack')
# # => git@localhost:rack.git
#
def submodule_url_for(ref, path)
if submodules(ref).any?
submodule = submodules(ref)[path]
if submodule
submodule['url']
end
end
end
def last_commit_for_path(sha, path)
2016-12-20 13:43:59 +00:00
sha = last_commit_id_for_path(sha, path)
commit(sha)
end
2014-04-09 13:09:11 +00:00
2016-12-15 05:40:15 +00:00
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)}"
2016-12-20 13:43:59 +00:00
2016-12-15 04:29:51 +00:00
cache.fetch(key) do
2016-12-20 13:43:59 +00:00
args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
Gitlab::Popen.popen(args, path_to_repo).first.strip
2016-12-15 04:29:51 +00:00
end
end
def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
result = n.match(/\A#{name}-([0-9]+)\z/)
result[1].to_i if result
end.compact
highest_branch_id = branch_ids.max || 0
return name if opts[:mild] && 0 == highest_branch_id
"#{name}-#{highest_branch_id + 1}"
end
2014-04-09 13:09:11 +00:00
# Remove archives older than 2 hours
def branches_sorted_by(value)
case value
when 'name'
branches.sort_by(&:name)
when 'updated_desc'
branches.sort do |a, b|
commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
end
when 'updated_asc'
branches.sort do |a, b|
commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
end
else
branches
end
end
2016-06-16 17:33:29 +00:00
def tags_sorted_by(value)
case value
when 'name'
VersionSorter.rsort(tags) { |tag| tag.name }
2016-06-16 17:33:29 +00:00
when 'updated_desc'
tags_sorted_by_committed_date.reverse
when 'updated_asc'
tags_sorted_by_committed_date
else
tags
end
end
def contributors
commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
commits.group_by(&:author_email).map do |email, commits|
2014-07-02 12:09:06 +00:00
contributor = Gitlab::Contributor.new
contributor.email = email
commits.each do |commit|
2014-07-02 12:09:06 +00:00
if contributor.name.blank?
contributor.name = commit.author_name
end
2014-07-02 12:09:06 +00:00
contributor.commits += 1
end
2014-07-02 12:09:06 +00:00
contributor
end
end
def ref_name_for_sha(ref_path, sha)
args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
2016-10-04 14:03:13 +00:00
# Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
Gitlab::Popen.popen(args, path_to_repo).first.split.last
end
def refs_contains_sha(ref_type, sha)
args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
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
2015-01-17 13:12:49 +00:00
def branch_names_contains(sha)
refs_contains_sha('branch', sha)
end
2015-01-17 13:12:49 +00:00
def tag_names_contains(sha)
refs_contains_sha('tag', sha)
2015-01-17 13:12:49 +00:00
end
def local_branches
@local_branches ||= raw_repository.local_branches
end
alias_method :branches, :local_branches
def tags
@tags ||= raw_repository.tags
end
# rubocop:disable Metrics/ParameterLists
2016-11-14 20:27:21 +00:00
def commit_dir(
user, path,
message:, branch_name:,
author_email: nil, author_name: nil,
source_branch_name: nil, source_project: project)
check_tree_entry_for_dir(branch_name, path)
if source_branch_name
source_project.repository.
check_tree_entry_for_dir(source_branch_name, path)
2016-12-07 17:12:49 +00:00
end
commit_file(
user,
"#{path}/.gitkeep",
2016-12-07 17:12:49 +00:00
'',
message: message,
branch_name: branch_name,
update: false,
2016-12-07 17:12:49 +00:00
author_email: author_email,
author_name: author_name,
source_branch_name: source_branch_name,
2016-12-07 17:12:49 +00:00
source_project: source_project)
end
# rubocop:enable Metrics/ParameterLists
2016-11-14 20:27:21 +00:00
# rubocop:disable Metrics/ParameterLists
def commit_file(
user, path, content,
message:, branch_name:, update: true,
author_email: nil, author_name: nil,
source_branch_name: nil, source_project: project)
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
if source_branch_name &&
2017-01-04 17:10:35 +00:00
source_project.repository.tree_entry_at(source_branch_name, path)
raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
end
end
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
source_branch_name: source_branch_name,
source_project: source_project,
2016-12-07 18:15:09 +00:00
actions: [{ action: :create,
file_path: path,
content: content }])
end
2016-11-14 20:27:21 +00:00
# rubocop:enable Metrics/ParameterLists
2016-11-14 20:27:21 +00:00
# rubocop:disable Metrics/ParameterLists
def update_file(
user, path, content,
message:, branch_name:, previous_path:,
author_email: nil, author_name: nil,
source_branch_name: nil, source_project: project)
action = if previous_path && previous_path != path
:move
else
:update
end
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
source_branch_name: source_branch_name,
source_project: source_project,
2016-12-07 18:15:09 +00:00
actions: [{ action: action,
file_path: path,
content: content,
previous_path: previous_path }])
end
2016-11-14 20:27:21 +00:00
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists
2016-11-14 20:27:21 +00:00
def remove_file(
user, path,
message:, branch_name:,
author_email: nil, author_name: nil,
source_branch_name: nil, source_project: project)
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
source_branch_name: source_branch_name,
source_project: source_project,
2016-12-07 18:15:09 +00:00
actions: [{ action: :delete,
file_path: path }])
end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists
2016-11-14 20:27:21 +00:00
def multi_action(
user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
source_branch_name: nil, source_project: project)
GitOperationService.new(user, self).with_branch(
branch_name,
source_branch_name: source_branch_name,
source_project: source_project) do |source_commit|
index = rugged.index
parents = if source_commit
index.read_tree(source_commit.raw_commit.tree)
[source_commit.sha]
else
[]
end
actions.each do |act|
git_action(index, act)
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
# rubocop:enable Metrics/ParameterLists
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
2016-09-20 16:07:52 +00:00
author = Gitlab::Git::committer_hash(email: email, name: name) || committer
{
author: author,
committer: committer
}
end
def user_to_committer(user)
Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
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
def merge(user, merge_request, options = {})
GitOperationService.new(user, self).with_branch(
merge_request.target_branch) do |source_commit|
our_commit = source_commit.sha
their_commit = merge_request.diff_head_sha
raise 'Invalid merge target' unless our_commit
raise 'Invalid merge source' unless their_commit
merge_index = rugged.merge_commits(our_commit, their_commit)
break if merge_index.conflicts?
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
)
commit_id = Rugged::Commit.create(rugged, actual_options)
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
rescue Repository::CommitError # when merge_index.conflicts?
false
2016-01-29 17:04:48 +00:00
end
def revert(
user, commit, branch_name, revert_tree_id = nil,
source_branch_name: nil, source_project: project)
revert_tree_id ||= check_revert_content(commit, branch_name)
return false unless revert_tree_id
GitOperationService.new(user, self).with_branch(
branch_name,
source_branch_name: source_branch_name,
source_project: source_project) do |source_commit|
committer = user_to_committer(user)
2016-11-14 20:46:54 +00:00
Rugged::Commit.create(rugged,
Merge branch 'jej-23867-use-mr-finder-instead-of-access-check' into 'security' Replace MR access checks with use of MergeRequestsFinder Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867 :warning: - Potentially untested :bomb: - No test coverage :traffic_light: - Test coverage of some sort exists (a test failed when error raised) :vertical_traffic_light: - Test coverage of return value (a test failed when nil used) :white_check_mark: - Permissions check tested - [x] :bomb: app/finders/notes_finder.rb:17 - [x] :warning: app/views/layouts/nav/_project.html.haml:80 [`.count`] - [x] :bomb: app/controllers/concerns/creates_commit.rb:84 - [x] :traffic_light: app/controllers/projects/commits_controller.rb:24 - [x] :traffic_light: app/controllers/projects/compare_controller.rb:56 - [x] :vertical_traffic_light: app/controllers/projects/discussions_controller.rb:29 - [x] :white_check_mark: app/controllers/projects/todos_controller.rb:27 - [x] :vertical_traffic_light: app/models/commit.rb:268 - [x] :white_check_mark: lib/gitlab/search_results.rb:71 - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_267_266 Memoize ` merged_merge_request(current_user)` - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_248_247 Expected side effect for `merged_merge_request!`, consider `skip_authorization: true`. - [x] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#d1c10892daedb4d4dd3d4b12b6d071091eea83df_269_269 Scary use of unchecked `merged_merge_request?` See merge request !2033
2016-11-29 13:47:43 +00:00
message: commit.revert_message(user),
2016-02-03 01:51:37 +00:00
author: committer,
committer: committer,
tree: revert_tree_id,
parents: [source_commit.sha])
2016-02-03 01:51:37 +00:00
end
end
def cherry_pick(
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)
return false unless cherry_pick_tree_id
GitOperationService.new(user, self).with_branch(
branch_name,
source_branch_name: source_branch_name,
source_project: source_project) do |source_commit|
committer = user_to_committer(user)
2016-11-14 20:46:54 +00:00
Rugged::Commit.create(rugged,
message: commit.message,
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [source_commit.sha])
end
end
def resolve_conflicts(user, branch_name, params)
GitOperationService.new(user, self).with_branch(branch_name) do
2016-08-04 13:20:04 +00:00
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
end
end
def check_revert_content(commit, branch_name)
source_sha = find_branch(branch_name).dereferenced_target.sha
args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit?
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
def check_cherry_pick_content(commit, branch_name)
source_sha = find_branch(branch_name).dereferenced_target.sha
args = [commit.id, source_sha]
args << 1 if commit.merge_commit?
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
def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0
end
2015-08-09 18:31:50 +00:00
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
if branch_commit
same_head = branch_commit.id == root_ref_commit.id
!same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
2015-08-09 18:31:50 +00:00
else
nil
end
end
2015-10-16 05:45:06 +00:00
def merge_base(first_commit_id, second_commit_id)
2016-02-11 14:52:37 +00:00
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
2015-10-16 05:45:06 +00:00
rugged.merge_base(first_commit_id, second_commit_id)
2016-01-28 14:10:48 +00:00
rescue Rugged::ReferenceError
nil
2015-10-16 05:45:06 +00:00
end
def is_ancestor?(ancestor_id, descendant_id)
merge_base(ancestor_id, descendant_id) == ancestor_id
end
2016-11-10 17:27:09 +00:00
def empty_repo?
!exists? || !has_visible_content?
end
def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank?
2016-11-08 12:21:19 +00:00
offset = 2
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})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
2016-11-10 17:27:09 +00:00
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
def with_repo_branch_commit(source_repository, source_branch_name)
branch_name_or_sha =
if source_repository == self
source_branch_name
else
tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
fetch_ref(
source_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}",
tmp_ref
)
source_repository.commit(source_branch_name).sha
end
yield(commit(branch_name_or_sha))
ensure
rugged.references.delete(tmp_ref) if tmp_ref
end
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
end
def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path)
end
def ls_files(ref)
actual_ref = ref || root_ref
raw_repository.ls_files(actual_ref)
end
def gitattribute(path, name)
raw_repository.attributes(path)[name]
end
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
# 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)
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
end
end
end
def cache_instance_variable_name(key)
:"@#{key.to_s.tr('?!', '')}"
end
def file_on_head(type)
if head = tree(:head)
head.blobs.find do |file|
Gitlab::FileDetector.type_of(file.name) == type
end
end
end
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
private
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
def refs_directory_exists?
return false unless path_with_namespace
File.exist?(File.join(path_to_repo, 'refs'))
end
2016-04-29 14:25:03 +00:00
def cache
@cache ||= RepositoryCache.new(path_with_namespace, @project.id)
2016-04-29 14:25:03 +00:00
end
2016-06-16 17:33:29 +00:00
def tags_sorted_by_committed_date
tags.sort_by { |tag| tag.dereferenced_target.committed_date }
2016-06-16 17:33:29 +00:00
end
2016-07-04 00:30:55 +00:00
def keep_around_ref_name(sha)
"refs/keep-around/#{sha}"
end
def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end
end