lib/rugged_adapter/git_layer_rugged.rb (547 lines of code) (raw):
# frozen_string_literal: true
require 'rugged'
require 'ostruct'
require 'mime-types'
module Gollum
def self.set_git_timeout(time)
end
def self.set_git_max_filesize(size)
end
module Git
DEFAULT_MIME_TYPE = "text/plain"
class NoSuchShaFound < StandardError; end
class Actor
attr_accessor :name, :email, :time
def self.default_actor
self.new("Gollum", "Gollum@wiki")
end
def initialize(name, email, time = nil)
@name = name
@email = email
@time = time
end
def output(time)
# implementation from grit
offset = time.utc_offset / 60
"%s <%s> %d %+.2d%.2d" % [
@name,
@email || "null",
time.to_i,
offset / 60,
offset.abs % 60]
end
def to_h
{:name => @name, :email => @email, :time => @time}
end
end
class Blob
attr_reader :mode
attr_reader :name
attr_reader :id
def self.create(repo, options)
blob = repo.git.lookup(options[:id])
self.new(blob, **options)
end
def initialize(blob, options = {})
@blob = blob
@mode = options[:mode]
@name = options[:name]
@size = options[:size]
@id = blob.oid
end
def data
@content ||= @blob.content
end
def is_symlink
@mode == 0120000
end
def mime_type
guesses = MIME::Types.type_for(self.name) rescue []
guesses.first ? guesses.first.simplified : DEFAULT_MIME_TYPE
end
def size
@size || @blob.size
end
def symlink_target(base_path = nil)
target = data
new_path = ::File.expand_path(::File.join('..', target), base_path)
return new_path if ::File.file? new_path
nil
end
end
class Commit
def initialize(commit, tracked_pathname = nil)
@commit = commit
@tracked_pathname = tracked_pathname
end
def id
@commit.oid
end
alias_method :sha, :id
alias_method :to_s, :id
attr_reader :commit, :tracked_pathname
def author
@author ||= Gollum::Git::Actor.new(@commit.author[:name], @commit.author[:email])
end
def authored_date
@commit.author[:time]
end
def message
@commit.message
end
def tree
Gollum::Git::Tree.new(@commit.tree)
end
def stats
@stats ||= build_stats
end
def parent
@commit.parents.empty? ? nil : Gollum::Git::Commit.new(@commit.parents.first)
end
private
def build_stats
additions = 0
deletions = 0
total = 0
files = []
parent = @commit.parents.first
diff = Rugged::Tree.diff(@commit.tree.repo, parent ? parent.tree : nil, @commit.tree)
diff = diff.each_patch do |patch|
new_additions = patch.additions
new_deletions = patch.deletions
additions += new_additions
deletions += new_deletions
total += patch.changes
files << [patch.delta.new_file[:path].force_encoding("UTF-8"), new_additions, new_deletions, patch.changes]
end
OpenStruct.new(:additions => additions, :deletions => deletions, :files => files, :id => id, :total => total)
end
end
class Git
# Rugged does not have a Git class, but the Repository class should allows us to do what's necessary.
def initialize(repo)
@repo = repo
end
def exist?
::File.exists?(@repo.path)
end
def grep(query, options={})
ref = options[:ref] ? options[:ref] : "HEAD"
tree = @repo.lookup(sha_from_ref(ref)).tree
tree = @repo.lookup(tree[options[:path]][:oid]) if options[:path]
enc = options.fetch(:encoding, 'utf-8')
results = []
tree.walk_blobs(:postorder) do |root, entry|
blob = @repo.lookup(entry[:oid])
count = 0
next if blob.binary?
blob.content.force_encoding(enc).each_line do |line|
next unless line.match(/#{query}/i)
count += 1
end
path = options[:path] ? ::File.join(options[:path], root, entry[:name]) : "#{root}#{entry[:name]}"
results << {:name => path, :count => count} unless count == 0
end
results
end
def rm(path, options = {})
index = @repo.index
index.write
::File.unlink ::File.join(@repo.workdir, path)
end
def cat_file(options, sha)
@repo.lookup(sha).read_raw
end
def apply_patch(head_sha = 'HEAD', patch=nil)
false # Rewrite gollum-lib's revert so that it doesn't require a direct equivalent of Grit's apply_patch
end
def revert(path, sha1, sha2, ref)
# FIXME: See https://github.com/gollum/grit_adapter/pull/14
fail NotImplementedError
end
def checkout(path, ref = 'HEAD', options = {})
path = path.nil? ? path : [path]
options = options.merge({:paths => path, :strategy => :force})
if ref == 'HEAD'
@repo.checkout_head(**options)
else
ref = "refs/heads/#{ref}" unless ref =~ /^refs\/heads\//
@repo.checkout_tree(sha_from_ref(ref), **options)
end
end
def log(ref = 'refs/heads/master', path = nil, options = {})
default_options = {
:limit => options[:max_count] ? options[:max_count] : 10,
:offset => options[:skip] ? options[:skip] : 0,
:path => path,
:follow => false,
:skip_merges => false
}
options = default_options.merge(**options)
options[:limit] ||= 0
options[:offset] ||= 0
sha = sha_from_ref(ref)
return [] if sha.nil?
begin
build_log(sha, **options)
rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
# Return an empty array if the ref wasn't found
[]
end
end
def versions_for_path(path = nil, ref = nil, options = {})
log(ref, path, **options)
end
def ls_files(query, options = {})
ref = options[:ref] || "refs/heads/master"
tree = @repo.lookup(sha_from_ref(ref)).tree
tree = @repo.lookup(tree[options[:path]][:oid]) if options[:path]
results = []
tree.walk_blobs do |root, blob|
next unless blob[:name] =~ /#{query}/
path = options[:path] ? ::File.join(options[:path], root, blob[:name]) : "#{root}#{blob[:name]}"
results << path
end
results
end
def lookup(id)
@repo.lookup(id)
end
def ref_to_sha(query)
return query if sha?(query)
query = "refs/heads/#{query}" if !query.nil? && !(query =~ /^refs\/heads\//) && !(query == "HEAD")
begin
return @repo.rev_parse_oid(query)
rescue Rugged::ReferenceError, Rugged::InvalidError
return nil
end
end
def sha_or_commit_from_ref(ref, request_kind = nil)
sha = ref_to_sha(ref)
return nil if sha.nil?
object = @repo.lookup(sha)
if object.kind_of?(Rugged::Commit) then
return Gollum::Git::Commit.new(object) if request_kind == :commit
sha
elsif object.respond_to?(:target)
sha_or_commit_from_ref(object.target.oid, request_kind)
end
end
alias_method :sha_from_ref, :sha_or_commit_from_ref
def commit_from_ref(ref)
sha_or_commit_from_ref(ref, :commit)
end
def push(remote, branches = nil, options = {})
branches = [branches].flatten.map {|branch| "refs/heads/#{branch}" unless branch =~ /^refs\/heads\//}
@repo.push(remote, branches, **options)
end
def pull(remote, branches = nil, options = {})
branches = [branches].flatten.map {|branch| "refs/heads/#{branch}" unless branch =~ /^refs\/heads\//}
r = @repo.remotes[remote]
r.fetch(branches, **options)
branches.each do |branch|
branch_name = branch.match(/^refs\/heads\/(.*)/)[1]
remote_name = remote.match(/^(refs\/heads\/)?(.*)/)[2]
remote_ref = @repo.branches["#{remote_name}/#{branch_name}"].target
local_ref = @repo.branches[branch].target
index = @repo.merge_commits(local_ref, remote_ref)
options = { author: Actor.default_actor.to_h,
committer: Actor.default_actor.to_h,
message: "Merged branch #{branch} of #{remote}.",
parents: [local_ref, remote_ref],
tree: index.write_tree(@repo),
update_ref: branch
}
Rugged::Commit.create @repo, **options
@repo.checkout(@repo.head.name, :strategy => :force) if !@repo.bare? && branch == @repo.head.name
end
end
private
def sha?(str)
!!(str =~ /^[0-9a-f]{40}$/)
end
# Return an array of log commits, given a SHA hash and a hash of
# options. From Gitlab::Git
def build_log(sha, options)
# Instantiate a Walker and add the SHA hash
walker = Rugged::Walker.new(@repo)
walker.push(sha)
commits = []
skipped = 0
current_path = options[:path].dup if options[:path]
current_path = nil if current_path == ''
renamed_path = current_path.nil? ? nil : current_path.dup
track_pathnames = true if current_path && options[:follow]
limit = options[:limit].to_i
offset = options[:offset].to_i
skip_merges = options[:skip_merges]
walker.sorting(Rugged::SORT_DATE)
walker.each do |c|
break if limit > 0 && commits.length >= limit
if skip_merges
# Skip merge commits
next if c.parents.length > 1
end
if !current_path || commit_touches_path?(c, current_path, options[:follow], walker)
# This is a commit we care about, unless we haven't skipped enough
# yet
skipped += 1
commits.push(Gollum::Git::Commit.new(c, track_pathnames ? renamed_path : nil)) if skipped > offset
renamed_path = current_path.nil? ? nil : current_path.dup
end
end
walker.reset
commits
end
# Returns true if +commit+ introduced changes to +path+, using commit
# trees to make that determination. Uses the history simplification
# rules that `git log` uses by default, where a commit is omitted if it
# is TREESAME to any parent.
#
# If the +follow+ option is true and the file specified by +path+ was
# renamed, then the path value is set to the old path.
def commit_touches_path?(commit, path, follow, walker)
entry = tree_entry(commit, path)
if commit.parents.empty?
# This is the root commit, return true if it has +path+ in its tree
return entry != nil
end
num_treesame = 0
commit.parents.each do |parent|
parent_entry = tree_entry(parent, path)
# Only follow the first TREESAME parent for merge commits
if num_treesame > 0
walker.hide(parent.oid)
next
end
if entry.nil? && parent_entry.nil?
num_treesame += 1
elsif entry && parent_entry && entry[:oid] == parent_entry[:oid]
num_treesame += 1
end
end
case num_treesame
when 0
detect_rename(commit, commit.parents.first, path) if follow
true
else false
end
end
# Find the entry for +path+ in the tree for +commit+
def tree_entry(commit, path)
pathname = Pathname.new(path)
tmp_entry = nil
pathname.each_filename do |dir|
tmp_entry = tmp_entry ? @repo.lookup(tmp_entry[:oid])[dir] : commit.tree[dir]
return nil unless tmp_entry
end
tmp_entry
end
# Compare +commit+ and +parent+ for +path+. If +path+ is a file and was
# renamed in +commit+, then set +path+ to the old filename.
def detect_rename(commit, parent, path)
diff = parent.diff(commit, paths: [path], disable_pathspec_match: true)
# If +path+ is a filename, not a directory, then we should only have
# one delta. We don't need to follow renames for directories.
return nil if diff.each_delta.count > 1
delta = diff.each_delta.first
if delta.added?
full_diff = parent.diff(commit)
full_diff.find_similar!
full_diff.each_delta do |full_delta|
if full_delta.renamed? && path == full_delta.new_file[:path]
# Look for the old path in ancestors
path.replace(full_delta.old_file[:path])
end
end
end
end
end
class Index
def initialize(index, repo)
@index = index
@rugged_repo = repo
@treemap = {}
end
def delete(path)
@index.remove(path)
update_treemap(path, false)
false
end
def delete_all(glob)
# if the path start with escape it and avoid treating it as a comment
escaped_glob = glob[0] == '#' ? glob.sub('#', '\#') : glob
@index.remove_all(escaped_glob)
update_treemap(glob, false)
false
end
def add(path, data)
blob = @rugged_repo.write(data, :blob)
@index.add(:path => path, :oid => blob, :mode => 0100644)
update_treemap(path, data)
data
end
def index
@index
end
def commit(message, parents = nil, actor = nil, last_tree = nil, head = 'refs/heads/master')
commit_options = {}
head = "refs/heads/#{head}" if head && head !~ %r(^refs/heads/)
parents = get_parents(parents, head) || []
actor = Gollum::Git::Actor.default_actor if actor.nil?
commit_options[:tree] = @index.write_tree
commit_options[:author] = actor.to_h
commit_options[:committer] = actor.to_h
commit_options[:message] = message.to_s
commit_options[:parents] = parents
commit_options[:update_ref] = head
Rugged::Commit.create(@rugged_repo, commit_options)
end
def tree
@treemap
end
def read_tree(id)
id = Gollum::Git::Git.new(@rugged_repo).ref_to_sha(id)
return nil if id.nil?
begin
current_tree = @rugged_repo.lookup(id)
current_tree = current_tree.tree unless current_tree.is_a?(Rugged::Tree)
@index.read_tree(current_tree)
rescue
raise Gollum::Git::NoSuchShaFound
end
@current_tree = Gollum::Git::Tree.new(current_tree)
end
def current_tree
@current_tree
end
private
def get_parents(parents, head)
if parents
parents.map{|parent| parent.commit}
elsif ref = @rugged_repo.references[head]
ref = ref.target
ref = ref.target if ref.respond_to?(:target)
[ref]
end
end
def update_treemap(path, data)
# From RJGit::Plumbing::Index
path = path[1..-1] if path[0] == ::File::SEPARATOR
path = path.split(::File::SEPARATOR)
last = path.pop
current = @treemap
path.each do |dir|
current[dir] ||= {}
node = current[dir]
current = node
end
current[last] = data
@treemap
end
end
class Ref
def initialize(ref)
@ref = ref
end
def name
@ref.name
end
def commit
Gollum::Git::Commit.new(@ref.target)
end
end
class Repo
def initialize(path, options)
@repo = Rugged::Repository.new(path, **options)
end
def self.init(path)
Rugged::Repository.init_at(path, false)
self.new(path, :is_bare => false)
end
def self.init_bare(path)
Rugged::Repository.init_at(path, true)
self.new(path, :is_bare => true)
end
def bare
@repo.bare?
end
def config
@repo.config
end
def git
@git ||= Gollum::Git::Git.new(@repo)
end
def commit(id)
begin
git.commit_from_ref(id)
rescue
raise Gollum::Git::NoSuchShaFound
end
end
def commits(start = 'refs/heads/master', max_count = 10, skip = 0)
git.log(start, nil, :max_count => max_count, :skip => skip)
end
def head
begin
return Gollum::Git::Ref.new(@repo.head)
rescue Rugged::ReferenceError
return nil
end
end
def index
@index ||= Gollum::Git::Index.new(@repo.index, @repo)
end
def diff(sha1, sha2, *paths)
opts = paths.nil? ? {} : {:paths => paths}
@repo.diff(sha1, sha2, opts).patch
end
def log(commit = 'refs/heads/master', path = nil, options = {})
git.log(commit, path, **options)
end
def lstree(sha, options = {})
results = []
@repo.lookup(sha).tree.walk(:postorder) do |root, entry|
entry[:sha] = entry[:oid]
entry[:mode] = entry[:filemode].to_s(8)
entry[:type] = entry[:type].to_s
entry[:path] = "#{root}#{entry[:name]}"
results << entry
end
results
end
def path
@repo.path
end
# Checkout branch and if necessary first create it. Currently used only in gollum-lib's tests.
def update_ref(ref, commit_sha)
ref = "refs/heads/#{ref}" unless ref =~ /^refs\/heads\//
if _ref = @repo.references[ref]
@repo.references.update(_ref, commit_sha)
else
@repo.create_branch(ref, commit_sha)
@repo.checkout(ref, :strategy => :force) unless @repo.bare?
end
end
def files_sorted_by_created_at(sha = nil)
sha ||= @repo.head.target.oid
file_renamings = {}
sorting = Rugged::SORT_DATE | Rugged::SORT_TOPO
@repo.walk(sha, sorting).with_object([]) do |commit, files|
parent = commit.parents.first
diff = commit.diff(parent, reverse: true)
diff.find_similar!
diff.each_delta do |delta|
name = delta.new_file[:path]
if delta.added?
files << (file_renamings[name] || name)
elsif delta.renamed?
file_renamings[delta.old_file[:path]] = file_renamings[name] || name
end
end
end
end
end
class Tree
def initialize(tree)
@tree = tree
end
def keys
@tree.map{|entry| entry[:name]}
end
def [](i)
@tree[i]
end
def id
@tree.oid
end
def /(file)
return self if file == '/'
begin
obj = @tree.path(file)
rescue Rugged::TreeError
return nil
end
return nil if obj.nil?
obj = @tree.owner.lookup(obj[:oid])
obj.is_a?(Rugged::Tree) ? Gollum::Git::Tree.new(obj) : Gollum::Git::Blob.new(obj)
end
def blobs
blobs = []
@tree.each_blob {|blob| blobs << Gollum::Git::Blob.new(@tree.owner.lookup(blob[:oid]), blob) }
blobs
end
end
end
end