Absorb gitlab_git
This commit is contained in:
parent
aec04a47c1
commit
a00578ce5c
6
Gemfile
6
Gemfile
|
|
@ -16,6 +16,8 @@ gem 'default_value_for', '~> 3.0.0'
|
|||
gem 'mysql2', '~> 0.3.16', group: :mysql
|
||||
gem 'pg', '~> 0.18.2', group: :postgres
|
||||
|
||||
gem 'rugged', '~> 0.24.0'
|
||||
|
||||
# Authentication libraries
|
||||
gem 'devise', '~> 4.2'
|
||||
gem 'doorkeeper', '~> 4.2.0'
|
||||
|
|
@ -49,10 +51,6 @@ gem 'u2f', '~> 0.2.1'
|
|||
# Browser detection
|
||||
gem 'browser', '~> 2.2'
|
||||
|
||||
# Extracting information from a git repository
|
||||
# Provide access to Gitlab::Git library
|
||||
gem 'gitlab_git', '~> 10.7.0'
|
||||
|
||||
# LDAP Auth
|
||||
# GitLab fork with several improvements to original library. For full list of changes
|
||||
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
|
||||
|
|
|
|||
|
|
@ -255,11 +255,6 @@ GEM
|
|||
mime-types (>= 1.16, < 3)
|
||||
posix-spawn (~> 0.3)
|
||||
gitlab-markup (1.5.0)
|
||||
gitlab_git (10.7.0)
|
||||
activesupport (~> 4.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
github-linguist (~> 4.7.0)
|
||||
rugged (~> 0.24.0)
|
||||
gitlab_omniauth-ldap (1.2.1)
|
||||
net-ldap (~> 0.9)
|
||||
omniauth (~> 1.0)
|
||||
|
|
@ -857,7 +852,6 @@ DEPENDENCIES
|
|||
github-linguist (~> 4.7.0)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.5.0)
|
||||
gitlab_git (~> 10.7.0)
|
||||
gitlab_omniauth-ldap (~> 1.2.1)
|
||||
gollum-lib (~> 4.2)
|
||||
gollum-rugged_adapter (~> 0.4.2)
|
||||
|
|
@ -942,6 +936,7 @@ DEPENDENCIES
|
|||
rubocop-rspec (~> 1.5.0)
|
||||
ruby-fogbugz (~> 0.2.1)
|
||||
ruby-prof (~> 0.16.2)
|
||||
rugged (~> 0.24.0)
|
||||
sanitize (~> 2.0)
|
||||
sass-rails (~> 5.0.6)
|
||||
scss_lint (~> 0.47.0)
|
||||
|
|
@ -988,4 +983,4 @@ DEPENDENCIES
|
|||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.6
|
||||
1.13.7
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
class MergeRequestDiff < ActiveRecord::Base
|
||||
include Sortable
|
||||
include Importable
|
||||
include EncodingHelper
|
||||
include Gitlab::Git::EncodingHelper
|
||||
|
||||
# Prevent store of diff if commits amount more then 500
|
||||
COMMITS_SAFE_SIZE = 100
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
# Class for parsing Git attribute files and extracting the attributes for
|
||||
# file patterns.
|
||||
#
|
||||
# Unlike Rugged this parser only needs a single IO call (a call to `open`),
|
||||
# vastly reducing the time spent in extracting attributes.
|
||||
#
|
||||
# This class _only_ supports parsing the attributes file located at
|
||||
# `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
|
||||
# (`.gitattributes` is copied to this particular path).
|
||||
#
|
||||
# Basic usage:
|
||||
#
|
||||
# attributes = Gitlab::Git::Attributes.new(some_repo.path)
|
||||
#
|
||||
# attributes.attributes('README.md') # => { "eol" => "lf }
|
||||
class Attributes
|
||||
# path - The path to the Git repository.
|
||||
def initialize(path)
|
||||
@path = File.expand_path(path)
|
||||
@patterns = nil
|
||||
end
|
||||
|
||||
# Returns all the Git attributes for the given path.
|
||||
#
|
||||
# path - A path to a file for which to get the attributes.
|
||||
#
|
||||
# Returns a Hash.
|
||||
def attributes(path)
|
||||
full_path = File.join(@path, path)
|
||||
|
||||
patterns.each do |pattern, attrs|
|
||||
return attrs if File.fnmatch?(pattern, full_path)
|
||||
end
|
||||
|
||||
{}
|
||||
end
|
||||
|
||||
# Returns a Hash containing the file patterns and their attributes.
|
||||
def patterns
|
||||
@patterns ||= parse_file
|
||||
end
|
||||
|
||||
# Parses an attribute string.
|
||||
#
|
||||
# These strings can be in the following formats:
|
||||
#
|
||||
# text # => { "text" => true }
|
||||
# -text # => { "text" => false }
|
||||
# key=value # => { "key" => "value" }
|
||||
#
|
||||
# string - The string to parse.
|
||||
#
|
||||
# Returns a Hash containing the attributes and their values.
|
||||
def parse_attributes(string)
|
||||
values = {}
|
||||
dash = '-'
|
||||
equal = '='
|
||||
binary = 'binary'
|
||||
|
||||
string.split(/\s+/).each do |chunk|
|
||||
# Data such as "foo = bar" should be treated as "foo" and "bar" being
|
||||
# separate boolean attributes.
|
||||
next if chunk == equal
|
||||
|
||||
key = chunk
|
||||
|
||||
# Input: "-foo"
|
||||
if chunk.start_with?(dash)
|
||||
key = chunk.byteslice(1, chunk.length - 1)
|
||||
value = false
|
||||
|
||||
# Input: "foo=bar"
|
||||
elsif chunk.include?(equal)
|
||||
key, value = chunk.split(equal, 2)
|
||||
|
||||
# Input: "foo"
|
||||
else
|
||||
value = true
|
||||
end
|
||||
|
||||
values[key] = value
|
||||
|
||||
# When the "binary" option is set the "diff" option should be set to
|
||||
# the inverse. If "diff" is later set it should overwrite the
|
||||
# automatically set value.
|
||||
values['diff'] = false if key == binary && value
|
||||
end
|
||||
|
||||
values
|
||||
end
|
||||
|
||||
# Iterates over every line in the attributes file.
|
||||
def each_line
|
||||
full_path = File.join(@path, 'info/attributes')
|
||||
|
||||
return unless File.exist?(full_path)
|
||||
|
||||
File.open(full_path, 'r') do |handle|
|
||||
handle.each_line do |line|
|
||||
break unless line.valid_encoding?
|
||||
|
||||
yield line.strip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Parses the Git attributes file.
|
||||
def parse_file
|
||||
pairs = []
|
||||
comment = '#'
|
||||
|
||||
each_line do |line|
|
||||
next if line.start_with?(comment) || line.empty?
|
||||
|
||||
pattern, attrs = line.split(/\s+/, 2)
|
||||
|
||||
parsed = attrs ? parse_attributes(attrs) : {}
|
||||
|
||||
pairs << [File.join(@path, pattern), parsed]
|
||||
end
|
||||
|
||||
# Newer entries take precedence over older entries.
|
||||
pairs.reverse.to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
require_relative 'encoding_helper'
|
||||
|
||||
module Gitlab
|
||||
module Git
|
||||
class Blame
|
||||
include Gitlab::Git::EncodingHelper
|
||||
|
||||
attr_reader :lines, :blames
|
||||
|
||||
def initialize(repository, sha, path)
|
||||
@repo = repository
|
||||
@sha = sha
|
||||
@path = path
|
||||
@lines = []
|
||||
@blames = load_blame
|
||||
end
|
||||
|
||||
def each
|
||||
@blames.each do |blame|
|
||||
yield(
|
||||
Gitlab::Git::Commit.new(blame.commit),
|
||||
blame.line
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_blame
|
||||
cmd = %W(git --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
|
||||
# Read in binary mode to ensure ASCII-8BIT
|
||||
raw_output = IO.popen(cmd, 'rb') {|io| io.read }
|
||||
output = encode_utf8(raw_output)
|
||||
process_raw_blame output
|
||||
end
|
||||
|
||||
def process_raw_blame(output)
|
||||
lines, final = [], []
|
||||
info, commits = {}, {}
|
||||
|
||||
# process the output
|
||||
output.split("\n").each do |line|
|
||||
if line[0, 1] == "\t"
|
||||
lines << line[1, line.size]
|
||||
elsif m = /^(\w{40}) (\d+) (\d+)/.match(line)
|
||||
commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i
|
||||
commits[commit_id] = nil unless commits.key?(commit_id)
|
||||
info[lineno] = [commit_id, old_lineno]
|
||||
end
|
||||
end
|
||||
|
||||
# load all commits in single call
|
||||
commits.keys.each do |key|
|
||||
commits[key] = @repo.lookup(key)
|
||||
end
|
||||
|
||||
# get it together
|
||||
info.sort.each do |lineno, (commit_id, old_lineno)|
|
||||
commit = commits[commit_id]
|
||||
final << BlameLine.new(lineno, old_lineno, commit, lines[lineno - 1])
|
||||
end
|
||||
|
||||
@lines = final
|
||||
end
|
||||
end
|
||||
|
||||
class BlameLine
|
||||
attr_accessor :lineno, :oldlineno, :commit, :line
|
||||
def initialize(lineno, oldlineno, commit, line)
|
||||
@lineno = lineno
|
||||
@oldlineno = oldlineno
|
||||
@commit = commit
|
||||
@line = line
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
require_relative 'encoding_helper'
|
||||
require_relative 'path_helper'
|
||||
|
||||
module Gitlab
|
||||
module Git
|
||||
class Blob
|
||||
include Linguist::BlobHelper
|
||||
include Gitlab::Git::EncodingHelper
|
||||
|
||||
# This number is the maximum amount of data that we want to display to
|
||||
# the user. We load as much as we can for encoding detection
|
||||
# (Linguist) and LFS pointer parsing. All other cases where we need full
|
||||
# blob data should use load_all_data!.
|
||||
MAX_DATA_DISPLAY_SIZE = 10485760
|
||||
|
||||
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
|
||||
|
||||
class << self
|
||||
def find(repository, sha, path)
|
||||
commit = repository.lookup(sha)
|
||||
root_tree = commit.tree
|
||||
|
||||
blob_entry = find_entry_by_path(repository, root_tree.oid, path)
|
||||
|
||||
return nil unless blob_entry
|
||||
|
||||
if blob_entry[:type] == :commit
|
||||
submodule_blob(blob_entry, path, sha)
|
||||
else
|
||||
blob = repository.lookup(blob_entry[:oid])
|
||||
|
||||
if blob
|
||||
Blob.new(
|
||||
id: blob.oid,
|
||||
name: blob_entry[:name],
|
||||
size: blob.size,
|
||||
data: blob.content(MAX_DATA_DISPLAY_SIZE),
|
||||
mode: blob_entry[:filemode].to_s(8),
|
||||
path: path,
|
||||
commit_id: sha,
|
||||
binary: blob.binary?
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def raw(repository, sha)
|
||||
blob = repository.lookup(sha)
|
||||
|
||||
Blob.new(
|
||||
id: blob.oid,
|
||||
size: blob.size,
|
||||
data: blob.content(MAX_DATA_DISPLAY_SIZE),
|
||||
binary: blob.binary?
|
||||
)
|
||||
end
|
||||
|
||||
# Recursive search of blob id by path
|
||||
#
|
||||
# Ex.
|
||||
# blog/ # oid: 1a
|
||||
# app/ # oid: 2a
|
||||
# models/ # oid: 3a
|
||||
# file.rb # oid: 4a
|
||||
#
|
||||
#
|
||||
# Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a'
|
||||
#
|
||||
def find_entry_by_path(repository, root_id, path)
|
||||
root_tree = repository.lookup(root_id)
|
||||
# Strip leading slashes
|
||||
path[/^\/*/] = ''
|
||||
path_arr = path.split('/')
|
||||
|
||||
entry = root_tree.find do |entry|
|
||||
entry[:name] == path_arr[0]
|
||||
end
|
||||
|
||||
return nil unless entry
|
||||
|
||||
if path_arr.size > 1
|
||||
return nil unless entry[:type] == :tree
|
||||
path_arr.shift
|
||||
find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
|
||||
else
|
||||
[:blob, :commit].include?(entry[:type]) ? entry : nil
|
||||
end
|
||||
end
|
||||
|
||||
def submodule_blob(blob_entry, path, sha)
|
||||
Blob.new(
|
||||
id: blob_entry[:oid],
|
||||
name: blob_entry[:name],
|
||||
data: '',
|
||||
path: path,
|
||||
commit_id: sha,
|
||||
)
|
||||
end
|
||||
|
||||
# Commit file in repository and return commit sha
|
||||
#
|
||||
# options should contain next structure:
|
||||
# file: {
|
||||
# content: 'Lorem ipsum...',
|
||||
# path: 'documents/story.txt',
|
||||
# update: true
|
||||
# },
|
||||
# author: {
|
||||
# email: 'user@example.com',
|
||||
# name: 'Test User',
|
||||
# time: Time.now
|
||||
# },
|
||||
# committer: {
|
||||
# email: 'user@example.com',
|
||||
# name: 'Test User',
|
||||
# time: Time.now
|
||||
# },
|
||||
# commit: {
|
||||
# message: 'Wow such commit',
|
||||
# branch: 'master',
|
||||
# update_ref: false
|
||||
# }
|
||||
#
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
def commit(repository, options, action = :add)
|
||||
file = options[:file]
|
||||
update = file[:update].nil? ? true : file[:update]
|
||||
author = options[:author]
|
||||
committer = options[:committer]
|
||||
commit = options[:commit]
|
||||
repo = repository.rugged
|
||||
ref = commit[:branch]
|
||||
update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
|
||||
parents = []
|
||||
mode = 0o100644
|
||||
|
||||
unless ref.start_with?('refs/')
|
||||
ref = 'refs/heads/' + ref
|
||||
end
|
||||
|
||||
path_name = PathHelper.normalize_path(file[:path])
|
||||
# Abort if any invalid characters remain (e.g. ../foo)
|
||||
raise Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
|
||||
|
||||
filename = path_name.to_s
|
||||
index = repo.index
|
||||
|
||||
unless repo.empty?
|
||||
rugged_ref = repo.references[ref]
|
||||
raise Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
|
||||
last_commit = rugged_ref.target
|
||||
index.read_tree(last_commit.tree)
|
||||
parents = [last_commit]
|
||||
end
|
||||
|
||||
if action == :remove
|
||||
index.remove(filename)
|
||||
else
|
||||
file_entry = index.get(filename)
|
||||
|
||||
if action == :rename
|
||||
old_path_name = PathHelper.normalize_path(file[:previous_path])
|
||||
old_filename = old_path_name.to_s
|
||||
file_entry = index.get(old_filename)
|
||||
index.remove(old_filename) unless file_entry.blank?
|
||||
end
|
||||
|
||||
if file_entry
|
||||
raise Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
|
||||
|
||||
# Preserve the current file mode if one is available
|
||||
mode = file_entry[:mode] if file_entry[:mode]
|
||||
end
|
||||
|
||||
content = file[:content]
|
||||
detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
|
||||
|
||||
unless detect && detect[:type] == :binary
|
||||
# When writing to the repo directly as we are doing here,
|
||||
# the `core.autocrlf` config isn't taken into account.
|
||||
content.gsub!("\r\n", "\n") if repository.autocrlf
|
||||
end
|
||||
|
||||
oid = repo.write(content, :blob)
|
||||
index.add(path: filename, oid: oid, mode: mode)
|
||||
end
|
||||
|
||||
opts = {}
|
||||
opts[:tree] = index.write_tree(repo)
|
||||
opts[:author] = author
|
||||
opts[:committer] = committer
|
||||
opts[:message] = commit[:message]
|
||||
opts[:parents] = parents
|
||||
opts[:update_ref] = ref if update_ref
|
||||
|
||||
Rugged::Commit.create(repo, opts)
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
|
||||
# Remove file from repository and return commit sha
|
||||
#
|
||||
# options should contain next structure:
|
||||
# file: {
|
||||
# path: 'documents/story.txt'
|
||||
# },
|
||||
# author: {
|
||||
# email: 'user@example.com',
|
||||
# name: 'Test User',
|
||||
# time: Time.now
|
||||
# },
|
||||
# committer: {
|
||||
# email: 'user@example.com',
|
||||
# name: 'Test User',
|
||||
# time: Time.now
|
||||
# },
|
||||
# commit: {
|
||||
# message: 'Remove FILENAME',
|
||||
# branch: 'master'
|
||||
# }
|
||||
#
|
||||
def remove(repository, options)
|
||||
commit(repository, options, :remove)
|
||||
end
|
||||
|
||||
# Rename file from repository and return commit sha
|
||||
#
|
||||
# options should contain next structure:
|
||||
# file: {
|
||||
# previous_path: 'documents/old_story.txt'
|
||||
# path: 'documents/story.txt'
|
||||
# content: 'Lorem ipsum...',
|
||||
# update: true
|
||||
# },
|
||||
# author: {
|
||||
# email: 'user@example.com',
|
||||
# name: 'Test User',
|
||||
# time: Time.now
|
||||
# },
|
||||
# committer: {
|
||||
# email: 'user@example.com',
|
||||
# name: 'Test User',
|
||||
# time: Time.now
|
||||
# },
|
||||
# commit: {
|
||||
# message: 'Rename FILENAME',
|
||||
# branch: 'master'
|
||||
# }
|
||||
#
|
||||
def rename(repository, options)
|
||||
commit(repository, options, :rename)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options)
|
||||
%w(id name path size data mode commit_id binary).each do |key|
|
||||
self.send("#{key}=", options[key.to_sym])
|
||||
end
|
||||
|
||||
@loaded_all_data = false
|
||||
# Retain the actual size before it is encoded
|
||||
@loaded_size = @data.bytesize if @data
|
||||
end
|
||||
|
||||
def binary?
|
||||
@binary.nil? ? super : @binary == true
|
||||
end
|
||||
|
||||
def empty?
|
||||
!data || data == ''
|
||||
end
|
||||
|
||||
def data
|
||||
encode! @data
|
||||
end
|
||||
|
||||
# Load all blob data (not just the first MAX_DATA_DISPLAY_SIZE bytes) into
|
||||
# memory as a Ruby string.
|
||||
def load_all_data!(repository)
|
||||
return if @data == '' # don't mess with submodule blobs
|
||||
return @data if @loaded_all_data
|
||||
|
||||
@loaded_all_data = true
|
||||
@data = repository.lookup(id).content
|
||||
@loaded_size = @data.bytesize
|
||||
end
|
||||
|
||||
def name
|
||||
encode! @name
|
||||
end
|
||||
|
||||
# Valid LFS object pointer is a text file consisting of
|
||||
# version
|
||||
# oid
|
||||
# size
|
||||
# see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
|
||||
def lfs_pointer?
|
||||
has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
|
||||
end
|
||||
|
||||
def lfs_oid
|
||||
if has_lfs_version_key?
|
||||
oid = data.match(/(?<=sha256:)([0-9a-f]{64})/)
|
||||
return oid[1] if oid
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def lfs_size
|
||||
if has_lfs_version_key?
|
||||
size = data.match(/(?<=size )([0-9]+)/)
|
||||
return size[1] if size
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def truncated?
|
||||
size && (size > loaded_size)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def has_lfs_version_key?
|
||||
!empty? && text? && data.start_with?("version https://git-lfs.github.com/spec")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class BlobSnippet
|
||||
include Linguist::BlobHelper
|
||||
|
||||
attr_accessor :ref
|
||||
attr_accessor :lines
|
||||
attr_accessor :filename
|
||||
attr_accessor :startline
|
||||
|
||||
def initialize(ref, lines, startline, filename)
|
||||
@ref, @lines, @startline, @filename = ref, lines, startline, filename
|
||||
end
|
||||
|
||||
def data
|
||||
lines.join("\n") if lines
|
||||
end
|
||||
|
||||
def name
|
||||
filename
|
||||
end
|
||||
|
||||
def size
|
||||
data.length
|
||||
end
|
||||
|
||||
def mode
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class Branch < Ref
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
# Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
|
||||
module Gitlab
|
||||
module Git
|
||||
class Commit
|
||||
include Gitlab::Git::EncodingHelper
|
||||
|
||||
attr_accessor :raw_commit, :head, :refs
|
||||
|
||||
SERIALIZE_KEYS = [
|
||||
:id, :message, :parent_ids,
|
||||
:authored_date, :author_name, :author_email,
|
||||
:committed_date, :committer_name, :committer_email
|
||||
].freeze
|
||||
|
||||
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
|
||||
|
||||
def ==(other)
|
||||
return false unless other.is_a?(Gitlab::Git::Commit)
|
||||
|
||||
methods = [:message, :parent_ids, :authored_date, :author_name,
|
||||
:author_email, :committed_date, :committer_name,
|
||||
:committer_email]
|
||||
|
||||
methods.all? do |method|
|
||||
send(method) == other.send(method)
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
# Get commits collection
|
||||
#
|
||||
# Ex.
|
||||
# Commit.where(
|
||||
# repo: repo,
|
||||
# ref: 'master',
|
||||
# path: 'app/models',
|
||||
# limit: 10,
|
||||
# offset: 5,
|
||||
# )
|
||||
#
|
||||
def where(options)
|
||||
repo = options.delete(:repo)
|
||||
raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log)
|
||||
|
||||
repo.log(options).map { |c| decorate(c) }
|
||||
end
|
||||
|
||||
# Get single commit
|
||||
#
|
||||
# Ex.
|
||||
# Commit.find(repo, '29eda46b')
|
||||
#
|
||||
# Commit.find(repo, 'master')
|
||||
#
|
||||
def find(repo, commit_id = "HEAD")
|
||||
return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
|
||||
|
||||
obj = if commit_id.is_a?(String)
|
||||
repo.rev_parse_target(commit_id)
|
||||
else
|
||||
Ref.dereference_object(commit_id)
|
||||
end
|
||||
|
||||
return nil unless obj.is_a?(Rugged::Commit)
|
||||
|
||||
decorate(obj)
|
||||
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository
|
||||
nil
|
||||
end
|
||||
|
||||
# Get last commit for HEAD
|
||||
#
|
||||
# Ex.
|
||||
# Commit.last(repo)
|
||||
#
|
||||
def last(repo)
|
||||
find(repo)
|
||||
end
|
||||
|
||||
# Get last commit for specified path and ref
|
||||
#
|
||||
# Ex.
|
||||
# Commit.last_for_path(repo, '29eda46b', 'app/models')
|
||||
#
|
||||
# Commit.last_for_path(repo, 'master', 'Gemfile')
|
||||
#
|
||||
def last_for_path(repo, ref, path = nil)
|
||||
where(
|
||||
repo: repo,
|
||||
ref: ref,
|
||||
path: path,
|
||||
limit: 1
|
||||
).first
|
||||
end
|
||||
|
||||
# Get commits between two revspecs
|
||||
# See also #repository.commits_between
|
||||
#
|
||||
# Ex.
|
||||
# Commit.between(repo, '29eda46b', 'master')
|
||||
#
|
||||
def between(repo, base, head)
|
||||
repo.commits_between(base, head).map do |commit|
|
||||
decorate(commit)
|
||||
end
|
||||
rescue Rugged::ReferenceError
|
||||
[]
|
||||
end
|
||||
|
||||
# Delegate Repository#find_commits
|
||||
def find_all(repo, options = {})
|
||||
repo.find_commits(options)
|
||||
end
|
||||
|
||||
def decorate(commit, ref = nil)
|
||||
Gitlab::Git::Commit.new(commit, ref)
|
||||
end
|
||||
|
||||
# Returns a diff object for the changes introduced by +rugged_commit+.
|
||||
# If +rugged_commit+ doesn't have a parent, then the diff is between
|
||||
# this commit and an empty repo. See Repository#diff for the keys
|
||||
# allowed in the +options+ hash.
|
||||
def diff_from_parent(rugged_commit, options = {})
|
||||
options ||= {}
|
||||
break_rewrites = options[:break_rewrites]
|
||||
actual_options = Diff.filter_diff_options(options)
|
||||
|
||||
diff = if rugged_commit.parents.empty?
|
||||
rugged_commit.diff(actual_options.merge(reverse: true))
|
||||
else
|
||||
rugged_commit.parents[0].diff(rugged_commit, actual_options)
|
||||
end
|
||||
|
||||
diff.find_similar!(break_rewrites: break_rewrites)
|
||||
diff
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(raw_commit, head = nil)
|
||||
raise "Nil as raw commit passed" unless raw_commit
|
||||
|
||||
if raw_commit.is_a?(Hash)
|
||||
init_from_hash(raw_commit)
|
||||
elsif raw_commit.is_a?(Rugged::Commit)
|
||||
init_from_rugged(raw_commit)
|
||||
else
|
||||
raise "Invalid raw commit type: #{raw_commit.class}"
|
||||
end
|
||||
|
||||
@head = head
|
||||
end
|
||||
|
||||
def sha
|
||||
id
|
||||
end
|
||||
|
||||
def short_id(length = 10)
|
||||
id.to_s[0..length]
|
||||
end
|
||||
|
||||
def safe_message
|
||||
@safe_message ||= message
|
||||
end
|
||||
|
||||
def created_at
|
||||
committed_date
|
||||
end
|
||||
|
||||
# Was this commit committed by a different person than the original author?
|
||||
def different_committer?
|
||||
author_name != committer_name || author_email != committer_email
|
||||
end
|
||||
|
||||
def parent_id
|
||||
parent_ids.first
|
||||
end
|
||||
|
||||
# Shows the diff between the commit's parent and the commit.
|
||||
#
|
||||
# Cuts out the header and stats from #to_patch and returns only the diff.
|
||||
def to_diff(options = {})
|
||||
diff_from_parent(options).patch
|
||||
end
|
||||
|
||||
# Returns a diff object for the changes from this commit's first parent.
|
||||
# If there is no parent, then the diff is between this commit and an
|
||||
# empty repo. See Repository#diff for keys allowed in the +options+
|
||||
# hash.
|
||||
def diff_from_parent(options = {})
|
||||
Commit.diff_from_parent(raw_commit, options)
|
||||
end
|
||||
|
||||
def has_zero_stats?
|
||||
stats.total.zero?
|
||||
rescue
|
||||
true
|
||||
end
|
||||
|
||||
def no_commit_message
|
||||
"--no commit message"
|
||||
end
|
||||
|
||||
def to_hash
|
||||
serialize_keys.map.with_object({}) do |key, hash|
|
||||
hash[key] = send(key)
|
||||
end
|
||||
end
|
||||
|
||||
def date
|
||||
committed_date
|
||||
end
|
||||
|
||||
def diffs(options = {})
|
||||
DiffCollection.new(diff_from_parent(options), options)
|
||||
end
|
||||
|
||||
def parents
|
||||
raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
|
||||
end
|
||||
|
||||
def tree
|
||||
raw_commit.tree
|
||||
end
|
||||
|
||||
def stats
|
||||
Gitlab::Git::CommitStats.new(self)
|
||||
end
|
||||
|
||||
def to_patch(options = {})
|
||||
begin
|
||||
raw_commit.to_mbox(options)
|
||||
rescue Rugged::InvalidError => ex
|
||||
if ex.message =~ /Commit \w+ is a merge commit/
|
||||
'Patch format is not currently supported for merge commits.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Get a collection of Rugged::Reference objects for this commit.
|
||||
#
|
||||
# Ex.
|
||||
# commit.ref(repo)
|
||||
#
|
||||
def refs(repo)
|
||||
repo.refs_hash[id]
|
||||
end
|
||||
|
||||
# Get ref names collection
|
||||
#
|
||||
# Ex.
|
||||
# commit.ref_names(repo)
|
||||
#
|
||||
def ref_names(repo)
|
||||
refs(repo).map do |ref|
|
||||
ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "")
|
||||
end
|
||||
end
|
||||
|
||||
def message
|
||||
encode! @message
|
||||
end
|
||||
|
||||
def author_name
|
||||
encode! @author_name
|
||||
end
|
||||
|
||||
def author_email
|
||||
encode! @author_email
|
||||
end
|
||||
|
||||
def committer_name
|
||||
encode! @committer_name
|
||||
end
|
||||
|
||||
def committer_email
|
||||
encode! @committer_email
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_from_hash(hash)
|
||||
raw_commit = hash.symbolize_keys
|
||||
|
||||
serialize_keys.each do |key|
|
||||
send("#{key}=", raw_commit[key])
|
||||
end
|
||||
end
|
||||
|
||||
def init_from_rugged(commit)
|
||||
author = commit.author
|
||||
committer = commit.committer
|
||||
|
||||
@raw_commit = commit
|
||||
@id = commit.oid
|
||||
@message = commit.message
|
||||
@authored_date = author[:time]
|
||||
@committed_date = committer[:time]
|
||||
@author_name = author[:name]
|
||||
@author_email = author[:email]
|
||||
@committer_name = committer[:name]
|
||||
@committer_email = committer[:email]
|
||||
@parent_ids = commit.parents.map(&:oid)
|
||||
end
|
||||
|
||||
def serialize_keys
|
||||
SERIALIZE_KEYS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Gitlab::Git::CommitStats counts the additions, deletions, and total changes
|
||||
# in a commit.
|
||||
module Gitlab
|
||||
module Git
|
||||
class CommitStats
|
||||
attr_reader :id, :additions, :deletions, :total
|
||||
|
||||
# Instantiate a CommitStats object
|
||||
def initialize(commit)
|
||||
@id = commit.id
|
||||
@additions = 0
|
||||
@deletions = 0
|
||||
@total = 0
|
||||
|
||||
diff = commit.diff_from_parent
|
||||
|
||||
diff.each_patch do |p|
|
||||
# TODO: Use the new Rugged convenience methods when they're released
|
||||
@additions += p.stat[0]
|
||||
@deletions += p.stat[1]
|
||||
@total += p.changes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class Compare
|
||||
attr_reader :head, :base, :straight
|
||||
|
||||
def initialize(repository, base, head, straight = false)
|
||||
@repository = repository
|
||||
@straight = straight
|
||||
|
||||
unless base && head
|
||||
@commits = []
|
||||
return
|
||||
end
|
||||
|
||||
@base = Gitlab::Git::Commit.find(repository, base.try(:strip))
|
||||
@head = Gitlab::Git::Commit.find(repository, head.try(:strip))
|
||||
|
||||
@commits = [] unless @base && @head
|
||||
@commits = [] if same
|
||||
end
|
||||
|
||||
def same
|
||||
@base && @head && @base.id == @head.id
|
||||
end
|
||||
|
||||
def commits
|
||||
return @commits if defined?(@commits)
|
||||
|
||||
@commits = Gitlab::Git::Commit.between(@repository, @base.id, @head.id)
|
||||
end
|
||||
|
||||
def diffs(options = {})
|
||||
unless @head && @base
|
||||
return Gitlab::Git::DiffCollection.new([])
|
||||
end
|
||||
|
||||
paths = options.delete(:paths) || []
|
||||
options[:straight] = @straight
|
||||
Gitlab::Git::Diff.between(@repository, @head.id, @base.id, options, *paths)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
# Gitlab::Git::Diff is a wrapper around native Rugged::Diff object
|
||||
module Gitlab
|
||||
module Git
|
||||
class Diff
|
||||
class TimeoutError < StandardError; end
|
||||
include Gitlab::Git::EncodingHelper
|
||||
|
||||
# Diff properties
|
||||
attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff
|
||||
|
||||
# Stats properties
|
||||
attr_accessor :new_file, :renamed_file, :deleted_file
|
||||
|
||||
attr_accessor :too_large
|
||||
|
||||
# The maximum size of a diff to display.
|
||||
DIFF_SIZE_LIMIT = 102400 # 100 KB
|
||||
|
||||
# The maximum size before a diff is collapsed.
|
||||
DIFF_COLLAPSE_LIMIT = 10240 # 10 KB
|
||||
|
||||
class << self
|
||||
def between(repo, head, base, options = {}, *paths)
|
||||
straight = options.delete(:straight) || false
|
||||
|
||||
common_commit = if straight
|
||||
base
|
||||
else
|
||||
# Only show what is new in the source branch
|
||||
# compared to the target branch, not the other way
|
||||
# around. The linex below with merge_base is
|
||||
# equivalent to diff with three dots (git diff
|
||||
# branch1...branch2) From the git documentation:
|
||||
# "git diff A...B" is equivalent to "git diff
|
||||
# $(git-merge-base A B) B"
|
||||
repo.merge_base_commit(head, base)
|
||||
end
|
||||
|
||||
options ||= {}
|
||||
actual_options = filter_diff_options(options)
|
||||
repo.diff(common_commit, head, actual_options, *paths)
|
||||
end
|
||||
|
||||
# Return a copy of the +options+ hash containing only keys that can be
|
||||
# passed to Rugged. Allowed options are:
|
||||
#
|
||||
# :max_size ::
|
||||
# An integer specifying the maximum byte size of a file before a it
|
||||
# will be treated as binary. The default value is 512MB.
|
||||
#
|
||||
# :context_lines ::
|
||||
# The number of unchanged lines that define the boundary of a hunk
|
||||
# (and to display before and after the actual changes). The default is
|
||||
# 3.
|
||||
#
|
||||
# :interhunk_lines ::
|
||||
# The maximum number of unchanged lines between hunk boundaries before
|
||||
# the hunks will be merged into a one. The default is 0.
|
||||
#
|
||||
# :old_prefix ::
|
||||
# The virtual "directory" to prefix to old filenames in hunk headers.
|
||||
# The default is "a".
|
||||
#
|
||||
# :new_prefix ::
|
||||
# The virtual "directory" to prefix to new filenames in hunk headers.
|
||||
# The default is "b".
|
||||
#
|
||||
# :reverse ::
|
||||
# If true, the sides of the diff will be reversed.
|
||||
#
|
||||
# :force_text ::
|
||||
# If true, all files will be treated as text, disabling binary
|
||||
# attributes & detection.
|
||||
#
|
||||
# :ignore_whitespace ::
|
||||
# If true, all whitespace will be ignored.
|
||||
#
|
||||
# :ignore_whitespace_change ::
|
||||
# If true, changes in amount of whitespace will be ignored.
|
||||
#
|
||||
# :ignore_whitespace_eol ::
|
||||
# If true, whitespace at end of line will be ignored.
|
||||
#
|
||||
# :ignore_submodules ::
|
||||
# if true, submodules will be excluded from the diff completely.
|
||||
#
|
||||
# :patience ::
|
||||
# If true, the "patience diff" algorithm will be used (currenlty
|
||||
# unimplemented).
|
||||
#
|
||||
# :include_ignored ::
|
||||
# If true, ignored files will be included in the diff.
|
||||
#
|
||||
# :include_untracked ::
|
||||
# If true, untracked files will be included in the diff.
|
||||
#
|
||||
# :include_unmodified ::
|
||||
# If true, unmodified files will be included in the diff.
|
||||
#
|
||||
# :recurse_untracked_dirs ::
|
||||
# Even if +:include_untracked+ is true, untracked directories will
|
||||
# only be marked with a single entry in the diff. If this flag is set
|
||||
# to true, all files under ignored directories will be included in the
|
||||
# diff, too.
|
||||
#
|
||||
# :disable_pathspec_match ::
|
||||
# If true, the given +*paths+ will be applied as exact matches,
|
||||
# instead of as fnmatch patterns.
|
||||
#
|
||||
# :deltas_are_icase ::
|
||||
# If true, filename comparisons will be made with case-insensitivity.
|
||||
#
|
||||
# :include_untracked_content ::
|
||||
# if true, untracked content will be contained in the the diff patch
|
||||
# text.
|
||||
#
|
||||
# :skip_binary_check ::
|
||||
# If true, diff deltas will be generated without spending time on
|
||||
# binary detection. This is useful to improve performance in cases
|
||||
# where the actual file content difference is not needed.
|
||||
#
|
||||
# :include_typechange ::
|
||||
# If true, type changes for files will not be interpreted as deletion
|
||||
# of the "old file" and addition of the "new file", but will generate
|
||||
# typechange records.
|
||||
#
|
||||
# :include_typechange_trees ::
|
||||
# Even if +:include_typechange+ is true, blob -> tree changes will
|
||||
# still usually be handled as a deletion of the blob. If this flag is
|
||||
# set to true, blob -> tree changes will be marked as typechanges.
|
||||
#
|
||||
# :ignore_filemode ::
|
||||
# If true, file mode changes will be ignored.
|
||||
#
|
||||
# :recurse_ignored_dirs ::
|
||||
# Even if +:include_ignored+ is true, ignored directories will only be
|
||||
# marked with a single entry in the diff. If this flag is set to true,
|
||||
# all files under ignored directories will be included in the diff,
|
||||
# too.
|
||||
def filter_diff_options(options, default_options = {})
|
||||
allowed_options = [:max_size, :context_lines, :interhunk_lines,
|
||||
:old_prefix, :new_prefix, :reverse, :force_text,
|
||||
:ignore_whitespace, :ignore_whitespace_change,
|
||||
:ignore_whitespace_eol, :ignore_submodules,
|
||||
:patience, :include_ignored, :include_untracked,
|
||||
:include_unmodified, :recurse_untracked_dirs,
|
||||
:disable_pathspec_match, :deltas_are_icase,
|
||||
:include_untracked_content, :skip_binary_check,
|
||||
:include_typechange, :include_typechange_trees,
|
||||
:ignore_filemode, :recurse_ignored_dirs, :paths,
|
||||
:max_files, :max_lines, :all_diffs, :no_collapse]
|
||||
|
||||
if default_options
|
||||
actual_defaults = default_options.dup
|
||||
actual_defaults.keep_if do |key|
|
||||
allowed_options.include?(key)
|
||||
end
|
||||
else
|
||||
actual_defaults = {}
|
||||
end
|
||||
|
||||
if options
|
||||
filtered_opts = options.dup
|
||||
filtered_opts.keep_if do |key|
|
||||
allowed_options.include?(key)
|
||||
end
|
||||
filtered_opts = actual_defaults.merge(filtered_opts)
|
||||
else
|
||||
filtered_opts = actual_defaults
|
||||
end
|
||||
|
||||
filtered_opts
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(raw_diff, collapse: false)
|
||||
case raw_diff
|
||||
when Hash
|
||||
init_from_hash(raw_diff, collapse: collapse)
|
||||
when Rugged::Patch, Rugged::Diff::Delta
|
||||
init_from_rugged(raw_diff, collapse: collapse)
|
||||
when nil
|
||||
raise "Nil as raw diff passed"
|
||||
else
|
||||
raise "Invalid raw diff type: #{raw_diff.class}"
|
||||
end
|
||||
end
|
||||
|
||||
def serialize_keys
|
||||
@serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large)
|
||||
end
|
||||
|
||||
def to_hash
|
||||
hash = {}
|
||||
|
||||
keys = serialize_keys
|
||||
|
||||
keys.each do |key|
|
||||
hash[key] = send(key)
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
def submodule?
|
||||
a_mode == '160000' || b_mode == '160000'
|
||||
end
|
||||
|
||||
def line_count
|
||||
@line_count ||= Util.count_lines(@diff)
|
||||
end
|
||||
|
||||
def too_large?
|
||||
if @too_large.nil?
|
||||
@too_large = @diff.bytesize >= DIFF_SIZE_LIMIT
|
||||
else
|
||||
@too_large
|
||||
end
|
||||
end
|
||||
|
||||
def collapsible?
|
||||
@diff.bytesize >= DIFF_COLLAPSE_LIMIT
|
||||
end
|
||||
|
||||
def prune_large_diff!
|
||||
@diff = ''
|
||||
@line_count = 0
|
||||
@too_large = true
|
||||
end
|
||||
|
||||
def collapsed?
|
||||
return @collapsed if defined?(@collapsed)
|
||||
false
|
||||
end
|
||||
|
||||
def prune_collapsed_diff!
|
||||
@diff = ''
|
||||
@line_count = 0
|
||||
@collapsed = true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_from_rugged(rugged, collapse: false)
|
||||
if rugged.is_a?(Rugged::Patch)
|
||||
init_from_rugged_patch(rugged, collapse: collapse)
|
||||
d = rugged.delta
|
||||
else
|
||||
d = rugged
|
||||
end
|
||||
|
||||
@new_path = encode!(d.new_file[:path])
|
||||
@old_path = encode!(d.old_file[:path])
|
||||
@a_mode = d.old_file[:mode].to_s(8)
|
||||
@b_mode = d.new_file[:mode].to_s(8)
|
||||
@new_file = d.added?
|
||||
@renamed_file = d.renamed?
|
||||
@deleted_file = d.deleted?
|
||||
end
|
||||
|
||||
def init_from_rugged_patch(patch, collapse: false)
|
||||
# Don't bother initializing diffs that are too large. If a diff is
|
||||
# binary we're not going to display anything so we skip the size check.
|
||||
return if !patch.delta.binary? && prune_large_patch(patch, collapse)
|
||||
|
||||
@diff = encode!(strip_diff_headers(patch.to_s))
|
||||
end
|
||||
|
||||
def init_from_hash(hash, collapse: false)
|
||||
raw_diff = hash.symbolize_keys
|
||||
|
||||
serialize_keys.each do |key|
|
||||
send(:"#{key}=", raw_diff[key.to_sym])
|
||||
end
|
||||
|
||||
prune_large_diff! if too_large?
|
||||
prune_collapsed_diff! if collapse && collapsible?
|
||||
end
|
||||
|
||||
# If the patch surpasses any of the diff limits it calls the appropiate
|
||||
# prune method and returns true. Otherwise returns false.
|
||||
def prune_large_patch(patch, collapse)
|
||||
size = 0
|
||||
|
||||
patch.each_hunk do |hunk|
|
||||
hunk.each_line do |line|
|
||||
size += line.content.bytesize
|
||||
|
||||
if size >= DIFF_SIZE_LIMIT
|
||||
prune_large_diff!
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if collapse && size >= DIFF_COLLAPSE_LIMIT
|
||||
prune_collapsed_diff!
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# Strip out the information at the beginning of the patch's text to match
|
||||
# Grit's output
|
||||
def strip_diff_headers(diff_text)
|
||||
# Delete everything up to the first line that starts with '---' or
|
||||
# 'Binary'
|
||||
diff_text.sub!(/\A.*?^(---|Binary)/m, '\1')
|
||||
|
||||
if diff_text.start_with?('---', 'Binary')
|
||||
diff_text
|
||||
else
|
||||
# If the diff_text did not contain a line starting with '---' or
|
||||
# 'Binary', return the empty string. No idea why; we are just
|
||||
# preserving behavior from before the refactor.
|
||||
''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class DiffCollection
|
||||
include Enumerable
|
||||
|
||||
DEFAULT_LIMITS = { max_files: 100, max_lines: 5000 }.freeze
|
||||
|
||||
def initialize(iterator, options = {})
|
||||
@iterator = iterator
|
||||
@max_files = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
|
||||
@max_lines = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
|
||||
@max_bytes = @max_files * 5120 # Average 5 KB per file
|
||||
@safe_max_files = [@max_files, DEFAULT_LIMITS[:max_files]].min
|
||||
@safe_max_lines = [@max_lines, DEFAULT_LIMITS[:max_lines]].min
|
||||
@safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file
|
||||
@all_diffs = !!options.fetch(:all_diffs, false)
|
||||
@no_collapse = !!options.fetch(:no_collapse, true)
|
||||
@deltas_only = !!options.fetch(:deltas_only, false)
|
||||
|
||||
@line_count = 0
|
||||
@byte_count = 0
|
||||
@overflow = false
|
||||
@array = Array.new
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
if @populated
|
||||
# @iterator.each is slower than just iterating the array in place
|
||||
@array.each(&block)
|
||||
elsif @deltas_only
|
||||
each_delta(&block)
|
||||
else
|
||||
each_patch(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def empty?
|
||||
!@iterator.any?
|
||||
end
|
||||
|
||||
def overflow?
|
||||
populate!
|
||||
!!@overflow
|
||||
end
|
||||
|
||||
def size
|
||||
@size ||= count # forces a loop using each method
|
||||
end
|
||||
|
||||
def real_size
|
||||
populate!
|
||||
|
||||
if @overflow
|
||||
"#{size}+"
|
||||
else
|
||||
size.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def decorate!
|
||||
collection = each_with_index do |element, i|
|
||||
@array[i] = yield(element)
|
||||
end
|
||||
@populated = true
|
||||
collection
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def populate!
|
||||
return if @populated
|
||||
|
||||
each { nil } # force a loop through all diffs
|
||||
@populated = true
|
||||
nil
|
||||
end
|
||||
|
||||
def over_safe_limits?(files)
|
||||
files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes
|
||||
end
|
||||
|
||||
def each_delta
|
||||
@iterator.each_delta.with_index do |delta, i|
|
||||
diff = Gitlab::Git::Diff.new(delta)
|
||||
|
||||
yield @array[i] = diff
|
||||
end
|
||||
end
|
||||
|
||||
def each_patch
|
||||
@iterator.each_with_index do |raw, i|
|
||||
# First yield cached Diff instances from @array
|
||||
if @array[i]
|
||||
yield @array[i]
|
||||
next
|
||||
end
|
||||
|
||||
# We have exhausted @array, time to create new Diff instances or stop.
|
||||
break if @overflow
|
||||
|
||||
if !@all_diffs && i >= @max_files
|
||||
@overflow = true
|
||||
break
|
||||
end
|
||||
|
||||
collapse = !@all_diffs && !@no_collapse
|
||||
|
||||
diff = Gitlab::Git::Diff.new(raw, collapse: collapse)
|
||||
|
||||
if collapse && over_safe_limits?(i)
|
||||
diff.prune_collapsed_diff!
|
||||
end
|
||||
|
||||
@line_count += diff.line_count
|
||||
@byte_count += diff.diff.bytesize
|
||||
|
||||
if !@all_diffs && (@line_count >= @max_lines || @byte_count >= @max_bytes)
|
||||
# This last Diff instance pushes us over the lines limit. We stop and
|
||||
# discard it.
|
||||
@overflow = true
|
||||
break
|
||||
end
|
||||
|
||||
yield @array[i] = diff
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
module EncodingHelper
|
||||
extend self
|
||||
|
||||
# This threshold is carefully tweaked to prevent usage of encodings detected
|
||||
# by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
|
||||
# we're better off sticking with utf8 encoding.
|
||||
# Reason: git diff can return strings with invalid utf8 byte sequences if it
|
||||
# truncates a diff in the middle of a multibyte character. In this case
|
||||
# CharlockHolmes will try to guess the encoding and will likely suggest an
|
||||
# obscure encoding with low confidence.
|
||||
# There is a lot more info with this merge request:
|
||||
# https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
|
||||
ENCODING_CONFIDENCE_THRESHOLD = 40
|
||||
|
||||
def encode!(message)
|
||||
return nil unless message.respond_to? :force_encoding
|
||||
|
||||
# if message is utf-8 encoding, just return it
|
||||
message.force_encoding("UTF-8")
|
||||
return message if message.valid_encoding?
|
||||
|
||||
# return message if message type is binary
|
||||
detect = CharlockHolmes::EncodingDetector.detect(message)
|
||||
return message.force_encoding("BINARY") if detect && detect[:type] == :binary
|
||||
|
||||
# force detected encoding if we have sufficient confidence.
|
||||
if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
|
||||
message.force_encoding(detect[:encoding])
|
||||
end
|
||||
|
||||
# encode and clean the bad chars
|
||||
message.replace clean(message)
|
||||
rescue
|
||||
encoding = detect ? detect[:encoding] : "unknown"
|
||||
"--broken encoding: #{encoding}"
|
||||
end
|
||||
|
||||
def encode_utf8(message)
|
||||
detect = CharlockHolmes::EncodingDetector.detect(message)
|
||||
if detect
|
||||
CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
|
||||
else
|
||||
clean(message)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean(message)
|
||||
message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
|
||||
.encode("UTF-8")
|
||||
.gsub("\0".encode("UTF-8"), "")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class PathHelper
|
||||
class << self
|
||||
def normalize_path(filename)
|
||||
# Strip all leading slashes so that //foo -> foo
|
||||
filename[/^\/*/] = ''
|
||||
|
||||
# Expand relative paths (e.g. foo/../bar)
|
||||
filename = Pathname.new(filename)
|
||||
filename.relative_path_from(Pathname.new(''))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
require 'open3'
|
||||
|
||||
module Gitlab
|
||||
module Git
|
||||
module Popen
|
||||
def popen(cmd, path)
|
||||
unless cmd.is_a?(Array)
|
||||
raise "System commands must be given as an array of strings"
|
||||
end
|
||||
|
||||
vars = { "PWD" => path }
|
||||
options = { chdir: path }
|
||||
|
||||
@cmd_output = ""
|
||||
@cmd_status = 0
|
||||
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
|
||||
@cmd_output << stdout.read
|
||||
@cmd_output << stderr.read
|
||||
@cmd_status = wait_thr.value.exitstatus
|
||||
end
|
||||
|
||||
[@cmd_output, @cmd_status]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class Ref
|
||||
include Gitlab::Git::EncodingHelper
|
||||
|
||||
# Branch or tag name
|
||||
# without "refs/tags|heads" prefix
|
||||
attr_reader :name
|
||||
|
||||
# Target sha.
|
||||
# Usually it is commit sha but in case
|
||||
# when tag reference on other tag it can be tag sha
|
||||
attr_reader :target
|
||||
|
||||
# Dereferenced target
|
||||
# Commit object to which the Ref points to
|
||||
attr_reader :dereferenced_target
|
||||
|
||||
# Extract branch name from full ref path
|
||||
#
|
||||
# Ex.
|
||||
# Ref.extract_branch_name('refs/heads/master') #=> 'master'
|
||||
def self.extract_branch_name(str)
|
||||
str.gsub(/\Arefs\/heads\//, '')
|
||||
end
|
||||
|
||||
def self.dereference_object(object)
|
||||
object = object.target while object.is_a?(Rugged::Tag::Annotation)
|
||||
|
||||
object
|
||||
end
|
||||
|
||||
def initialize(repository, name, target)
|
||||
encode! name
|
||||
@name = name.gsub(/\Arefs\/(tags|heads)\//, '')
|
||||
@dereferenced_target = Commit.find(repository, target)
|
||||
@target = if target.respond_to?(:oid)
|
||||
target.oid
|
||||
elsif target.respond_to?(:name)
|
||||
target.name
|
||||
elsif target.is_a? String
|
||||
target
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,17 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class Tag < Ref
|
||||
attr_reader :object_sha
|
||||
|
||||
def initialize(repository, name, target, message = nil)
|
||||
super(repository, name, target)
|
||||
|
||||
@message = message
|
||||
end
|
||||
|
||||
def message
|
||||
encode! @message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
class Tree
|
||||
include Gitlab::Git::EncodingHelper
|
||||
|
||||
attr_accessor :id, :root_id, :name, :path, :type,
|
||||
:mode, :commit_id, :submodule_url
|
||||
|
||||
class << self
|
||||
# Get list of tree objects
|
||||
# for repository based on commit sha and path
|
||||
# Uses rugged for raw objects
|
||||
def where(repository, sha, path = nil)
|
||||
path = nil if path == '' || path == '/'
|
||||
|
||||
commit = repository.lookup(sha)
|
||||
root_tree = commit.tree
|
||||
|
||||
tree = if path
|
||||
id = Tree.find_id_by_path(repository, root_tree.oid, path)
|
||||
if id
|
||||
repository.lookup(id)
|
||||
else
|
||||
[]
|
||||
end
|
||||
else
|
||||
root_tree
|
||||
end
|
||||
|
||||
tree.map do |entry|
|
||||
Tree.new(
|
||||
id: entry[:oid],
|
||||
root_id: root_tree.oid,
|
||||
name: entry[:name],
|
||||
type: entry[:type],
|
||||
mode: entry[:filemode],
|
||||
path: path ? File.join(path, entry[:name]) : entry[:name],
|
||||
commit_id: sha,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Recursive search of tree id for path
|
||||
#
|
||||
# Ex.
|
||||
# blog/ # oid: 1a
|
||||
# app/ # oid: 2a
|
||||
# models/ # oid: 3a
|
||||
# views/ # oid: 4a
|
||||
#
|
||||
#
|
||||
# Tree.find_id_by_path(repo, '1a', 'app/models') # => '3a'
|
||||
#
|
||||
def find_id_by_path(repository, root_id, path)
|
||||
root_tree = repository.lookup(root_id)
|
||||
path_arr = path.split('/')
|
||||
|
||||
entry = root_tree.find do |entry|
|
||||
entry[:name] == path_arr[0] && entry[:type] == :tree
|
||||
end
|
||||
|
||||
return nil unless entry
|
||||
|
||||
if path_arr.size > 1
|
||||
path_arr.shift
|
||||
find_id_by_path(repository, entry[:oid], path_arr.join('/'))
|
||||
else
|
||||
entry[:oid]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options)
|
||||
%w(id root_id name path type mode commit_id).each do |key|
|
||||
self.send("#{key}=", options[key.to_sym])
|
||||
end
|
||||
end
|
||||
|
||||
def name
|
||||
encode! @name
|
||||
end
|
||||
|
||||
def dir?
|
||||
type == :tree
|
||||
end
|
||||
|
||||
def file?
|
||||
type == :blob
|
||||
end
|
||||
|
||||
def submodule?
|
||||
type == :commit
|
||||
end
|
||||
|
||||
def readme?
|
||||
name =~ /^readme/i
|
||||
end
|
||||
|
||||
def contributing?
|
||||
name =~ /^contributing/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
module Gitlab
|
||||
module Git
|
||||
module Util
|
||||
LINE_SEP = "\n".freeze
|
||||
|
||||
def self.count_lines(string)
|
||||
case string[-1]
|
||||
when nil
|
||||
0
|
||||
when LINE_SEP
|
||||
string.count(LINE_SEP)
|
||||
else
|
||||
string.count(LINE_SEP) + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Git::Attributes, seed_helper: true do
|
||||
let(:path) do
|
||||
File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git')
|
||||
end
|
||||
|
||||
subject { described_class.new(path) }
|
||||
|
||||
describe '#attributes' do
|
||||
context 'using a path with attributes' do
|
||||
it 'returns the attributes as a Hash' do
|
||||
expect(subject.attributes('test.txt')).to eq({ 'text' => true })
|
||||
end
|
||||
|
||||
it 'returns a Hash containing multiple attributes' do
|
||||
expect(subject.attributes('test.sh')).
|
||||
to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' })
|
||||
end
|
||||
|
||||
it 'returns a Hash containing attributes for a file with multiple extensions' do
|
||||
expect(subject.attributes('test.haml.html')).
|
||||
to eq({ 'gitlab-language' => 'haml' })
|
||||
end
|
||||
|
||||
it 'returns a Hash containing attributes for a file in a directory' do
|
||||
expect(subject.attributes('foo/bar.txt')).to eq({ 'foo' => true })
|
||||
end
|
||||
|
||||
it 'returns a Hash containing attributes with query string parameters' do
|
||||
expect(subject.attributes('foo.cgi')).
|
||||
to eq({ 'key' => 'value?p1=v1&p2=v2' })
|
||||
end
|
||||
|
||||
it 'returns a Hash containing the attributes for an absolute path' do
|
||||
expect(subject.attributes('/test.txt')).to eq({ 'text' => true })
|
||||
end
|
||||
|
||||
it 'returns a Hash containing the attributes when a pattern is defined using an absolute path' do
|
||||
# When a path is given without a leading slash it should still match
|
||||
# patterns defined with a leading slash.
|
||||
expect(subject.attributes('foo.png')).
|
||||
to eq({ 'gitlab-language' => 'png' })
|
||||
|
||||
expect(subject.attributes('/foo.png')).
|
||||
to eq({ 'gitlab-language' => 'png' })
|
||||
end
|
||||
|
||||
it 'returns an empty Hash for a defined path without attributes' do
|
||||
expect(subject.attributes('bla/bla.txt')).to eq({})
|
||||
end
|
||||
|
||||
context 'when the "binary" option is set for a path' do
|
||||
it 'returns true for the "binary" option' do
|
||||
expect(subject.attributes('test.binary')['binary']).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for the "diff" option' do
|
||||
expect(subject.attributes('test.binary')['diff']).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a path without any attributes' do
|
||||
it 'returns an empty Hash' do
|
||||
expect(subject.attributes('test.foo')).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#patterns' do
|
||||
it 'parses a file with entries' do
|
||||
expect(subject.patterns).to be_an_instance_of(Hash)
|
||||
end
|
||||
|
||||
it 'parses an entry that uses a tab to separate the pattern and attributes' do
|
||||
expect(subject.patterns[File.join(path, '*.md')]).
|
||||
to eq({ 'gitlab-language' => 'markdown' })
|
||||
end
|
||||
|
||||
it 'stores patterns in reverse order' do
|
||||
first = subject.patterns.to_a[0]
|
||||
|
||||
expect(first[0]).to eq(File.join(path, 'bla/bla.txt'))
|
||||
end
|
||||
|
||||
# It's a bit hard to test for something _not_ being processed. As such we'll
|
||||
# just test the number of entries.
|
||||
it 'ignores any comments and empty lines' do
|
||||
expect(subject.patterns.length).to eq(10)
|
||||
end
|
||||
|
||||
it 'does not parse anything when the attributes file does not exist' do
|
||||
expect(File).to receive(:exist?).
|
||||
with(File.join(path, 'info/attributes')).
|
||||
and_return(false)
|
||||
|
||||
expect(subject.patterns).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_attributes' do
|
||||
it 'parses a boolean attribute' do
|
||||
expect(subject.parse_attributes('text')).to eq({ 'text' => true })
|
||||
end
|
||||
|
||||
it 'parses a negated boolean attribute' do
|
||||
expect(subject.parse_attributes('-text')).to eq({ 'text' => false })
|
||||
end
|
||||
|
||||
it 'parses a key-value pair' do
|
||||
expect(subject.parse_attributes('foo=bar')).to eq({ 'foo' => 'bar' })
|
||||
end
|
||||
|
||||
it 'parses multiple attributes' do
|
||||
input = 'boolean key=value -negated'
|
||||
|
||||
expect(subject.parse_attributes(input)).
|
||||
to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false })
|
||||
end
|
||||
|
||||
it 'parses attributes with query string parameters' do
|
||||
expect(subject.parse_attributes('foo=bar?baz=1')).
|
||||
to eq({ 'foo' => 'bar?baz=1' })
|
||||
end
|
||||
end
|
||||
|
||||
describe '#each_line' do
|
||||
it 'iterates over every line in the attributes file' do
|
||||
args = [String] * 14 # the number of lines in the file
|
||||
|
||||
expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
|
||||
end
|
||||
|
||||
it 'does not yield when the attributes file does not exist' do
|
||||
expect(File).to receive(:exist?).
|
||||
with(File.join(path, 'info/attributes')).
|
||||
and_return(false)
|
||||
|
||||
expect { |b| subject.each_line(&b) }.not_to yield_control
|
||||
end
|
||||
|
||||
it 'does not yield when the attributes file has an unsupported encoding' do
|
||||
path = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git')
|
||||
attrs = described_class.new(path)
|
||||
|
||||
expect { |b| attrs.each_line(&b) }.not_to yield_control
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# coding: utf-8
|
||||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Blame, seed_helper: true do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
let(:blame) do
|
||||
Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md")
|
||||
end
|
||||
|
||||
context "each count" do
|
||||
it do
|
||||
data = []
|
||||
blame.each do |commit, line|
|
||||
data << {
|
||||
commit: commit,
|
||||
line: line
|
||||
}
|
||||
end
|
||||
|
||||
expect(data.size).to eq(95)
|
||||
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
|
||||
expect(data.first[:line]).to eq("# Contribute to GitLab")
|
||||
end
|
||||
end
|
||||
|
||||
context "ISO-8859 encoding" do
|
||||
let(:blame) do
|
||||
Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
|
||||
end
|
||||
|
||||
it 'converts to UTF-8' do
|
||||
data = []
|
||||
blame.each do |commit, line|
|
||||
data << {
|
||||
commit: commit,
|
||||
line: line
|
||||
}
|
||||
end
|
||||
|
||||
expect(data.size).to eq(1)
|
||||
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
|
||||
expect(data.first[:line]).to eq("Ä ü")
|
||||
end
|
||||
end
|
||||
|
||||
context "unknown encoding" do
|
||||
let(:blame) do
|
||||
Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
|
||||
end
|
||||
|
||||
it 'converts to UTF-8' do
|
||||
expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil)
|
||||
data = []
|
||||
blame.each do |commit, line|
|
||||
data << {
|
||||
commit: commit,
|
||||
line: line
|
||||
}
|
||||
end
|
||||
|
||||
expect(data.size).to eq(1)
|
||||
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
|
||||
expect(data.first[:line]).to eq(" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# encoding: UTF-8
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::BlobSnippet, seed_helper: true do
|
||||
describe :data do
|
||||
context 'empty lines' do
|
||||
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) }
|
||||
|
||||
it { expect(snippet.data).to be_nil }
|
||||
end
|
||||
|
||||
context 'present lines' do
|
||||
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') }
|
||||
|
||||
it { expect(snippet.data).to eq("wow\nmuch") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
# encoding: utf-8
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Blob, seed_helper: true do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
|
||||
describe :initialize do
|
||||
let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
|
||||
|
||||
it 'handles nil data' do
|
||||
expect(blob.name).to eq('test')
|
||||
expect(blob.size).to eq(nil)
|
||||
expect(blob.loaded_size).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe :find do
|
||||
context 'file in subdir' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
|
||||
|
||||
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
|
||||
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
|
||||
it { expect(blob.path).to eq("files/ruby/popen.rb") }
|
||||
it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
|
||||
it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
|
||||
it { expect(blob.size).to eq(669) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
end
|
||||
|
||||
context 'file in root' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") }
|
||||
|
||||
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
|
||||
it { expect(blob.name).to eq(".gitignore") }
|
||||
it { expect(blob.path).to eq(".gitignore") }
|
||||
it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
|
||||
it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
|
||||
it { expect(blob.size).to eq(241) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
it { expect(blob).not_to be_binary }
|
||||
end
|
||||
|
||||
context 'file in root with leading slash' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") }
|
||||
|
||||
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
|
||||
it { expect(blob.name).to eq(".gitignore") }
|
||||
it { expect(blob.path).to eq(".gitignore") }
|
||||
it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
|
||||
it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
|
||||
it { expect(blob.size).to eq(241) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
end
|
||||
|
||||
context 'non-exist file' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") }
|
||||
|
||||
it { expect(blob).to be_nil }
|
||||
end
|
||||
|
||||
context 'six submodule' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') }
|
||||
|
||||
it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
|
||||
it { expect(blob.data).to eq('') }
|
||||
|
||||
it 'does not get messed up by load_all_data!' do
|
||||
blob.load_all_data!(repository)
|
||||
expect(blob.data).to eq('')
|
||||
end
|
||||
|
||||
it 'does not mark the blob as binary' do
|
||||
expect(blob).not_to be_binary
|
||||
end
|
||||
end
|
||||
|
||||
context 'large file' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') }
|
||||
let(:blob_size) { 111803 }
|
||||
|
||||
it { expect(blob.size).to eq(blob_size) }
|
||||
it { expect(blob.data.length).to eq(blob_size) }
|
||||
|
||||
it 'check that this test is sane' do
|
||||
expect(blob.size).to be <= Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE
|
||||
end
|
||||
|
||||
it 'can load all data' do
|
||||
blob.load_all_data!(repository)
|
||||
expect(blob.data.length).to eq(blob_size)
|
||||
end
|
||||
|
||||
it 'marks the blob as binary' do
|
||||
expect(Gitlab::Git::Blob).to receive(:new).
|
||||
with(hash_including(binary: true)).
|
||||
and_call_original
|
||||
|
||||
expect(blob).to be_binary
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :raw do
|
||||
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
|
||||
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
|
||||
it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
|
||||
it { expect(raw_blob.size).to eq(669) }
|
||||
it { expect(raw_blob.truncated?).to be_falsey }
|
||||
|
||||
context 'large file' do
|
||||
it 'limits the size of a large file' do
|
||||
blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1
|
||||
buffer = Array.new(blob_size, 0)
|
||||
rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join(''))
|
||||
blob = Gitlab::Git::Blob.raw(repository, rugged_blob)
|
||||
|
||||
expect(blob.size).to eq(blob_size)
|
||||
expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
|
||||
expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
|
||||
expect(blob.truncated?).to be_truthy
|
||||
|
||||
blob.load_all_data!(repository)
|
||||
expect(blob.loaded_size).to eq(blob_size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'encoding' do
|
||||
context 'file with russian text' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
|
||||
|
||||
it { expect(blob.name).to eq("russian.rb") }
|
||||
it { expect(blob.data.lines.first).to eq("Хороший файл") }
|
||||
it { expect(blob.size).to eq(23) }
|
||||
it { expect(blob.truncated?).to be_falsey }
|
||||
# Run it twice since data is encoded after the first run
|
||||
it { expect(blob.truncated?).to be_falsey }
|
||||
it { expect(blob.mode).to eq("100755") }
|
||||
end
|
||||
|
||||
context 'file with Chinese text' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
|
||||
|
||||
it { expect(blob.name).to eq("テスト.txt") }
|
||||
it { expect(blob.data).to include("これはテスト") }
|
||||
it { expect(blob.size).to eq(340) }
|
||||
it { expect(blob.mode).to eq("100755") }
|
||||
it { expect(blob.truncated?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'file with ISO-8859 text' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") }
|
||||
|
||||
it { expect(blob.name).to eq("iso8859.txt") }
|
||||
it { expect(blob.loaded_size).to eq(4) }
|
||||
it { expect(blob.size).to eq(4) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
it { expect(blob.truncated?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'mode' do
|
||||
context 'file regular' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
|
||||
'files/ruby/regex.rb'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.name).to eq('regex.rb') }
|
||||
it { expect(blob.path).to eq('files/ruby/regex.rb') }
|
||||
it { expect(blob.size).to eq(1200) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
end
|
||||
|
||||
context 'file binary' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
|
||||
'files/executables/ls'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.name).to eq('ls') }
|
||||
it { expect(blob.path).to eq('files/executables/ls') }
|
||||
it { expect(blob.size).to eq(110080) }
|
||||
it { expect(blob.mode).to eq("100755") }
|
||||
end
|
||||
|
||||
context 'file symlink to regular' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
|
||||
'files/links/ruby-style-guide.md'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.name).to eq('ruby-style-guide.md') }
|
||||
it { expect(blob.path).to eq('files/links/ruby-style-guide.md') }
|
||||
it { expect(blob.size).to eq(31) }
|
||||
it { expect(blob.mode).to eq("120000") }
|
||||
end
|
||||
|
||||
context 'file symlink to binary' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
|
||||
'files/links/touch'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.name).to eq('touch') }
|
||||
it { expect(blob.path).to eq('files/links/touch') }
|
||||
it { expect(blob.size).to eq(20) }
|
||||
it { expect(blob.mode).to eq("120000") }
|
||||
end
|
||||
end
|
||||
|
||||
describe :commit do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
|
||||
let(:commit_options) do
|
||||
{
|
||||
file: {
|
||||
content: 'Lorem ipsum...',
|
||||
path: 'documents/story.txt'
|
||||
},
|
||||
author: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
committer: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
commit: {
|
||||
message: 'Wow such commit',
|
||||
branch: 'fix-mode'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) }
|
||||
let(:commit) { repository.lookup(commit_sha) }
|
||||
|
||||
it 'should add file with commit' do
|
||||
# Commit message valid
|
||||
expect(commit.message).to eq('Wow such commit')
|
||||
|
||||
tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' }
|
||||
|
||||
# Directory was created
|
||||
expect(tree[:type]).to eq(:tree)
|
||||
|
||||
# File was created
|
||||
expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt')
|
||||
end
|
||||
|
||||
describe "ref updating" do
|
||||
it 'creates a commit but does not udate a ref' do
|
||||
commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false}
|
||||
commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts)
|
||||
commit = repository.lookup(commit_sha)
|
||||
|
||||
# Commit message valid
|
||||
expect(commit.message).to eq('Wow such commit')
|
||||
|
||||
# Does not update any related ref
|
||||
expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid)
|
||||
expect(repository.lookup("HEAD").oid).not_to eq(commit.oid)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'reject updates' do
|
||||
it 'should reject updates' do
|
||||
commit_options[:file][:update] = false
|
||||
commit_options[:file][:path] = 'files/executables/ls'
|
||||
|
||||
expect{ commit_sha }.to raise_error('Filename already exists; update not allowed')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'file modes' do
|
||||
it 'should preserve file modes with commit' do
|
||||
commit_options[:file][:path] = 'files/executables/ls'
|
||||
|
||||
entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path])
|
||||
expect(entry[:filemode]).to eq(0100755)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :rename do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) }
|
||||
let(:rename_options) do
|
||||
{
|
||||
file: {
|
||||
path: 'NEWCONTRIBUTING.md',
|
||||
previous_path: 'CONTRIBUTING.md',
|
||||
content: 'Lorem ipsum...',
|
||||
update: true
|
||||
},
|
||||
author: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
committer: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
commit: {
|
||||
message: 'Rename readme',
|
||||
branch: 'master'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:rename_options2) do
|
||||
{
|
||||
file: {
|
||||
content: 'Lorem ipsum...',
|
||||
path: 'bin/new_executable',
|
||||
previous_path: 'bin/executable',
|
||||
},
|
||||
author: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
committer: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
commit: {
|
||||
message: 'Updates toberenamed.txt',
|
||||
branch: 'master',
|
||||
update_ref: false
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'maintains file permissions when renaming' do
|
||||
mode = 0o100755
|
||||
|
||||
Gitlab::Git::Blob.rename(repository, rename_options2)
|
||||
|
||||
expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode)
|
||||
end
|
||||
|
||||
it 'renames the file with commit and not change file permissions' do
|
||||
ref = rename_options[:commit][:branch]
|
||||
|
||||
expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil
|
||||
expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1)
|
||||
|
||||
expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil
|
||||
expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe :remove do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
|
||||
let(:commit_options) do
|
||||
{
|
||||
file: {
|
||||
path: 'README.md'
|
||||
},
|
||||
author: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
committer: {
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
time: Time.now
|
||||
},
|
||||
commit: {
|
||||
message: 'Remove readme',
|
||||
branch: 'feature'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) }
|
||||
let(:commit) { repository.lookup(commit_sha) }
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") }
|
||||
|
||||
it 'should remove file with commit' do
|
||||
# Commit message valid
|
||||
expect(commit.message).to eq('Remove readme')
|
||||
|
||||
# File was removed
|
||||
expect(blob).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe :lfs_pointers do
|
||||
context 'file a valid lfs pointer' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
|
||||
'files/lfs/image.jpg'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.lfs_pointer?).to eq(true) }
|
||||
it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") }
|
||||
it { expect(blob.lfs_size).to eq("19548") }
|
||||
it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") }
|
||||
it { expect(blob.name).to eq("image.jpg") }
|
||||
it { expect(blob.path).to eq("files/lfs/image.jpg") }
|
||||
it { expect(blob.size).to eq(130) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
end
|
||||
|
||||
describe 'file an invalid lfs pointer' do
|
||||
context 'with correct version header but incorrect size and oid' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
|
||||
'files/lfs/archive-invalid.tar'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.lfs_pointer?).to eq(false) }
|
||||
it { expect(blob.lfs_oid).to eq(nil) }
|
||||
it { expect(blob.lfs_size).to eq(nil) }
|
||||
it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") }
|
||||
it { expect(blob.name).to eq("archive-invalid.tar") }
|
||||
it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") }
|
||||
it { expect(blob.size).to eq(43) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
end
|
||||
|
||||
context 'with correct version header and size but incorrect size and oid' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
|
||||
'files/lfs/picture-invalid.png'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.lfs_pointer?).to eq(false) }
|
||||
it { expect(blob.lfs_oid).to eq(nil) }
|
||||
it { expect(blob.lfs_size).to eq("1575078") }
|
||||
it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") }
|
||||
it { expect(blob.name).to eq("picture-invalid.png") }
|
||||
it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
|
||||
it { expect(blob.size).to eq(57) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
end
|
||||
|
||||
context 'with correct version header and size but invalid size and oid' do
|
||||
let(:blob) do
|
||||
Gitlab::Git::Blob.find(
|
||||
repository,
|
||||
'33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
|
||||
'files/lfs/file-invalid.zip'
|
||||
)
|
||||
end
|
||||
|
||||
it { expect(blob.lfs_pointer?).to eq(false) }
|
||||
it { expect(blob.lfs_oid).to eq(nil) }
|
||||
it { expect(blob.lfs_size).to eq(nil) }
|
||||
it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") }
|
||||
it { expect(blob.name).to eq("file-invalid.zip") }
|
||||
it { expect(blob.path).to eq("files/lfs/file-invalid.zip") }
|
||||
it { expect(blob.size).to eq(60) }
|
||||
it { expect(blob.mode).to eq("100644") }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Branch, seed_helper: true do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
|
||||
subject { repository.branches }
|
||||
|
||||
it { is_expected.to be_kind_of Array }
|
||||
|
||||
describe '#size' do
|
||||
subject { super().size }
|
||||
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
|
||||
end
|
||||
|
||||
describe 'first branch' do
|
||||
let(:branch) { repository.branches.first }
|
||||
|
||||
it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) }
|
||||
it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
|
||||
end
|
||||
|
||||
describe 'master branch' do
|
||||
let(:branch) do
|
||||
repository.branches.find { |branch| branch.name == 'master' }
|
||||
end
|
||||
|
||||
it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) }
|
||||
end
|
||||
|
||||
it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
|
||||
end
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Commit, seed_helper: true do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
|
||||
let(:rugged_commit) do
|
||||
repository.rugged.lookup(SeedRepo::Commit::ID)
|
||||
end
|
||||
|
||||
describe "Commit info" do
|
||||
before do
|
||||
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
|
||||
|
||||
@committer = {
|
||||
email: 'mike@smith.com',
|
||||
name: "Mike Smith",
|
||||
time: Time.now
|
||||
}
|
||||
|
||||
@author = {
|
||||
email: 'john@smith.com',
|
||||
name: "John Smith",
|
||||
time: Time.now
|
||||
}
|
||||
|
||||
@parents = [repo.head.target]
|
||||
@gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) }
|
||||
@tree = @parents.first.tree
|
||||
|
||||
sha = Rugged::Commit.create(
|
||||
repo,
|
||||
author: @author,
|
||||
committer: @committer,
|
||||
tree: @tree,
|
||||
parents: @parents,
|
||||
message: "Refactoring specs",
|
||||
update_ref: "HEAD"
|
||||
)
|
||||
|
||||
@raw_commit = repo.lookup(sha)
|
||||
@commit = Gitlab::Git::Commit.new(@raw_commit)
|
||||
end
|
||||
|
||||
it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) }
|
||||
it { expect(@commit.id).to eq(@raw_commit.oid) }
|
||||
it { expect(@commit.sha).to eq(@raw_commit.oid) }
|
||||
it { expect(@commit.safe_message).to eq(@raw_commit.message) }
|
||||
it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) }
|
||||
it { expect(@commit.date).to eq(@raw_commit.committer[:time]) }
|
||||
it { expect(@commit.author_email).to eq(@author[:email]) }
|
||||
it { expect(@commit.author_name).to eq(@author[:name]) }
|
||||
it { expect(@commit.committer_name).to eq(@committer[:name]) }
|
||||
it { expect(@commit.committer_email).to eq(@committer[:email]) }
|
||||
it { expect(@commit.different_committer?).to be_truthy }
|
||||
it { expect(@commit.parents).to eq(@gitlab_parents) }
|
||||
it { expect(@commit.parent_id).to eq(@parents.first.oid) }
|
||||
it { expect(@commit.no_commit_message).to eq("--no commit message") }
|
||||
it { expect(@commit.tree).to eq(@tree) }
|
||||
|
||||
after do
|
||||
# Erase the new commit so other tests get the original repo
|
||||
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
|
||||
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
|
||||
end
|
||||
end
|
||||
|
||||
context 'Class methods' do
|
||||
describe :find do
|
||||
it "should return first head commit if without params" do
|
||||
expect(Gitlab::Git::Commit.last(repository).id).to eq(
|
||||
repository.raw.head.target.oid
|
||||
)
|
||||
end
|
||||
|
||||
it "should return valid commit" do
|
||||
expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
|
||||
end
|
||||
|
||||
it "should return valid commit for tag" do
|
||||
expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
|
||||
end
|
||||
|
||||
it "should return nil for non-commit ids" do
|
||||
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
|
||||
expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil
|
||||
end
|
||||
|
||||
it "should return nil for parent of non-commit object" do
|
||||
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
|
||||
expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil
|
||||
end
|
||||
|
||||
it "should return nil for nonexisting ids" do
|
||||
expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil
|
||||
end
|
||||
|
||||
context 'with broken repo' do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_BROKEN_REPO_PATH) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :last_for_path do
|
||||
context 'no path' do
|
||||
subject { Gitlab::Git::Commit.last_for_path(repository, 'master') }
|
||||
|
||||
describe '#id' do
|
||||
subject { super().id }
|
||||
it { is_expected.to eq(SeedRepo::LastCommit::ID) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'path' do
|
||||
subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') }
|
||||
|
||||
describe '#id' do
|
||||
subject { super().id }
|
||||
it { is_expected.to eq(SeedRepo::Commit::ID) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'ref + path' do
|
||||
subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
|
||||
|
||||
describe '#id' do
|
||||
subject { super().id }
|
||||
it { is_expected.to eq(SeedRepo::BigCommit::ID) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "where" do
|
||||
context 'path is empty string' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.where(
|
||||
repo: repository,
|
||||
ref: 'master',
|
||||
path: '',
|
||||
limit: 10
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 10 elements' do
|
||||
expect(subject.size).to eq(10)
|
||||
end
|
||||
it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
|
||||
end
|
||||
|
||||
context 'path is nil' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.where(
|
||||
repo: repository,
|
||||
ref: 'master',
|
||||
path: nil,
|
||||
limit: 10
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 10 elements' do
|
||||
expect(subject.size).to eq(10)
|
||||
end
|
||||
it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
|
||||
end
|
||||
|
||||
context 'ref is branch name' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.where(
|
||||
repo: repository,
|
||||
ref: 'master',
|
||||
path: 'files',
|
||||
limit: 3,
|
||||
offset: 1
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 3 elements' do
|
||||
expect(subject.size).to eq(3)
|
||||
end
|
||||
it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") }
|
||||
it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") }
|
||||
end
|
||||
|
||||
context 'ref is commit id' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.where(
|
||||
repo: repository,
|
||||
ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
|
||||
path: 'files',
|
||||
limit: 3,
|
||||
offset: 1
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 3 elements' do
|
||||
expect(subject.size).to eq(3)
|
||||
end
|
||||
it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") }
|
||||
it { is_expected.not_to include(SeedRepo::Commit::ID) }
|
||||
end
|
||||
|
||||
context 'ref is tag' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.where(
|
||||
repo: repository,
|
||||
ref: 'v1.0.0',
|
||||
path: 'files',
|
||||
limit: 3,
|
||||
offset: 1
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 3 elements' do
|
||||
expect(subject.size).to eq(3)
|
||||
end
|
||||
it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
|
||||
it { is_expected.not_to include(SeedRepo::Commit::ID) }
|
||||
end
|
||||
end
|
||||
|
||||
describe :between do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 1 element' do
|
||||
expect(subject.size).to eq(1)
|
||||
end
|
||||
it { is_expected.to include(SeedRepo::Commit::ID) }
|
||||
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
|
||||
end
|
||||
|
||||
describe :find_all do
|
||||
context 'max_count' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.find_all(
|
||||
repository,
|
||||
max_count: 50
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 31 elements' do
|
||||
expect(subject.size).to eq(33)
|
||||
end
|
||||
it { is_expected.to include(SeedRepo::Commit::ID) }
|
||||
it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
|
||||
it { is_expected.to include(SeedRepo::FirstCommit::ID) }
|
||||
end
|
||||
|
||||
context 'ref + max_count + skip' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.find_all(
|
||||
repository,
|
||||
ref: 'master',
|
||||
max_count: 50,
|
||||
skip: 1
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 23 elements' do
|
||||
expect(subject.size).to eq(24)
|
||||
end
|
||||
it { is_expected.to include(SeedRepo::Commit::ID) }
|
||||
it { is_expected.to include(SeedRepo::FirstCommit::ID) }
|
||||
it { is_expected.not_to include(SeedRepo::LastCommit::ID) }
|
||||
end
|
||||
|
||||
context 'contains feature + max_count' do
|
||||
subject do
|
||||
commits = Gitlab::Git::Commit.find_all(
|
||||
repository,
|
||||
contains: 'feature',
|
||||
max_count: 7
|
||||
)
|
||||
|
||||
commits.map { |c| c.id }
|
||||
end
|
||||
|
||||
it 'has 7 elements' do
|
||||
expect(subject.size).to eq(7)
|
||||
end
|
||||
|
||||
it { is_expected.not_to include(SeedRepo::Commit::PARENT_ID) }
|
||||
it { is_expected.not_to include(SeedRepo::Commit::ID) }
|
||||
it { is_expected.to include(SeedRepo::BigCommit::ID) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :init_from_rugged do
|
||||
let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
|
||||
subject { gitlab_commit }
|
||||
|
||||
describe '#id' do
|
||||
subject { super().id }
|
||||
it { is_expected.to eq(SeedRepo::Commit::ID) }
|
||||
end
|
||||
end
|
||||
|
||||
describe :init_from_hash do
|
||||
let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
|
||||
subject { commit }
|
||||
|
||||
describe '#id' do
|
||||
subject { super().id }
|
||||
it { is_expected.to eq(sample_commit_hash[:id])}
|
||||
end
|
||||
|
||||
describe '#message' do
|
||||
subject { super().message }
|
||||
it { is_expected.to eq(sample_commit_hash[:message])}
|
||||
end
|
||||
end
|
||||
|
||||
describe :stats do
|
||||
subject { commit.stats }
|
||||
|
||||
describe '#additions' do
|
||||
subject { super().additions }
|
||||
it { is_expected.to eq(11) }
|
||||
end
|
||||
|
||||
describe '#deletions' do
|
||||
subject { super().deletions }
|
||||
it { is_expected.to eq(6) }
|
||||
end
|
||||
end
|
||||
|
||||
describe :to_diff do
|
||||
subject { commit.to_diff }
|
||||
|
||||
it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
|
||||
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
|
||||
end
|
||||
|
||||
describe :has_zero_stats? do
|
||||
it { expect(commit.has_zero_stats?).to eq(false) }
|
||||
end
|
||||
|
||||
describe :to_patch do
|
||||
subject { commit.to_patch }
|
||||
|
||||
it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
|
||||
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
|
||||
end
|
||||
|
||||
describe :to_hash do
|
||||
let(:hash) { commit.to_hash }
|
||||
subject { hash }
|
||||
|
||||
it { is_expected.to be_kind_of Hash }
|
||||
|
||||
describe '#keys' do
|
||||
subject { super().keys.sort }
|
||||
it { is_expected.to match(sample_commit_hash.keys.sort) }
|
||||
end
|
||||
end
|
||||
|
||||
describe :diffs do
|
||||
subject { commit.diffs }
|
||||
|
||||
it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
|
||||
it { expect(subject.count).to eq(2) }
|
||||
it { expect(subject.first).to be_kind_of Gitlab::Git::Diff }
|
||||
end
|
||||
|
||||
describe :ref_names do
|
||||
let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
|
||||
subject { commit.ref_names(repository) }
|
||||
|
||||
it 'has 1 element' do
|
||||
expect(subject.size).to eq(1)
|
||||
end
|
||||
it { is_expected.to include("master") }
|
||||
it { is_expected.not_to include("feature") }
|
||||
end
|
||||
|
||||
def sample_commit_hash
|
||||
{
|
||||
author_email: "dmitriy.zaporozhets@gmail.com",
|
||||
author_name: "Dmitriy Zaporozhets",
|
||||
authored_date: "2012-02-27 20:51:12 +0200",
|
||||
committed_date: "2012-02-27 20:51:12 +0200",
|
||||
committer_email: "dmitriy.zaporozhets@gmail.com",
|
||||
committer_name: "Dmitriy Zaporozhets",
|
||||
id: SeedRepo::Commit::ID,
|
||||
message: "tree css fixes",
|
||||
parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Compare, seed_helper: true do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) }
|
||||
let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) }
|
||||
|
||||
describe :commits do
|
||||
subject do
|
||||
compare.commits.map(&:id)
|
||||
end
|
||||
|
||||
it 'has 8 elements' do
|
||||
expect(subject.size).to eq(8)
|
||||
end
|
||||
|
||||
it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
|
||||
it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
|
||||
|
||||
context 'non-existing base ref' do
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'non-existing head ref' do
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'base ref is equal to head ref' do
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'providing nil as base ref or head ref' do
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, nil, nil) }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
end
|
||||
|
||||
describe :diffs do
|
||||
subject do
|
||||
compare.diffs.map(&:new_path)
|
||||
end
|
||||
|
||||
it 'has 10 elements' do
|
||||
expect(subject.size).to eq(10)
|
||||
end
|
||||
|
||||
it { is_expected.to include('files/ruby/popen.rb') }
|
||||
it { is_expected.not_to include('LICENSE') }
|
||||
|
||||
context 'non-existing base ref' do
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'non-existing head ref' do
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
end
|
||||
|
||||
describe :same do
|
||||
subject do
|
||||
compare.same
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
|
||||
context 'base ref is equal to head ref' do
|
||||
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
end
|
||||
end
|
||||
|
||||
describe :commits_straight do
|
||||
subject do
|
||||
compare_straight.commits.map(&:id)
|
||||
end
|
||||
|
||||
it 'has 8 elements' do
|
||||
expect(subject.size).to eq(8)
|
||||
end
|
||||
|
||||
it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
|
||||
it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
|
||||
end
|
||||
|
||||
describe :diffs_straight do
|
||||
subject do
|
||||
compare_straight.diffs.map(&:new_path)
|
||||
end
|
||||
|
||||
it 'has 10 elements' do
|
||||
expect(subject.size).to eq(10)
|
||||
end
|
||||
|
||||
it { is_expected.to include('files/ruby/popen.rb') }
|
||||
it { is_expected.not_to include('LICENSE') }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,460 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Git::DiffCollection, seed_helper: true do
|
||||
subject do
|
||||
Gitlab::Git::DiffCollection.new(
|
||||
iterator,
|
||||
max_files: max_files,
|
||||
max_lines: max_lines,
|
||||
all_diffs: all_diffs,
|
||||
no_collapse: no_collapse
|
||||
)
|
||||
end
|
||||
let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) }
|
||||
let(:file_count) { 0 }
|
||||
let(:line_length) { 1 }
|
||||
let(:line_count) { 1 }
|
||||
let(:max_files) { 10 }
|
||||
let(:max_lines) { 100 }
|
||||
let(:all_diffs) { false }
|
||||
let(:no_collapse) { true }
|
||||
|
||||
describe '#to_a' do
|
||||
subject { super().to_a }
|
||||
it { is_expected.to be_kind_of ::Array }
|
||||
end
|
||||
|
||||
describe :decorate! do
|
||||
let(:file_count) { 3 }
|
||||
|
||||
it 'modifies the array in place' do
|
||||
count = 0
|
||||
subject.decorate! { |d| !d.nil? && count += 1 }
|
||||
expect(subject.to_a).to eq([1, 2, 3])
|
||||
expect(count).to eq(3)
|
||||
end
|
||||
|
||||
it 'avoids future iterator iterations' do
|
||||
subject.decorate! { |d| d unless d.nil? }
|
||||
|
||||
expect(iterator).not_to receive(:each)
|
||||
|
||||
subject.overflow?
|
||||
end
|
||||
end
|
||||
|
||||
context 'overflow handling' do
|
||||
context 'adding few enough files' do
|
||||
let(:file_count) { 3 }
|
||||
|
||||
context 'and few enough lines' do
|
||||
let(:line_count) { 10 }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('3') }
|
||||
end
|
||||
it { expect(subject.size).to eq(3) }
|
||||
|
||||
context 'when limiting is disabled' do
|
||||
let(:all_diffs) { true }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('3') }
|
||||
end
|
||||
it { expect(subject.size).to eq(3) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'and too many lines' do
|
||||
let(:line_count) { 1000 }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('0+') }
|
||||
end
|
||||
it { expect(subject.size).to eq(0) }
|
||||
|
||||
context 'when limiting is disabled' do
|
||||
let(:all_diffs) { true }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('3') }
|
||||
end
|
||||
it { expect(subject.size).to eq(3) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'adding too many files' do
|
||||
let(:file_count) { 11 }
|
||||
|
||||
context 'and few enough lines' do
|
||||
let(:line_count) { 1 }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('10+') }
|
||||
end
|
||||
it { expect(subject.size).to eq(10) }
|
||||
|
||||
context 'when limiting is disabled' do
|
||||
let(:all_diffs) { true }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('11') }
|
||||
end
|
||||
it { expect(subject.size).to eq(11) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'and too many lines' do
|
||||
let(:line_count) { 30 }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('3+') }
|
||||
end
|
||||
it { expect(subject.size).to eq(3) }
|
||||
|
||||
context 'when limiting is disabled' do
|
||||
let(:all_diffs) { true }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('11') }
|
||||
end
|
||||
it { expect(subject.size).to eq(11) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'adding exactly the maximum number of files' do
|
||||
let(:file_count) { 10 }
|
||||
|
||||
context 'and few enough lines' do
|
||||
let(:line_count) { 1 }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('10') }
|
||||
end
|
||||
it { expect(subject.size).to eq(10) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'adding too many bytes' do
|
||||
let(:file_count) { 10 }
|
||||
let(:line_length) { 5200 }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('9+') }
|
||||
end
|
||||
it { expect(subject.size).to eq(9) }
|
||||
|
||||
context 'when limiting is disabled' do
|
||||
let(:all_diffs) { true }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('10') }
|
||||
end
|
||||
it { expect(subject.size).to eq(10) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'empty collection' do
|
||||
subject { Gitlab::Git::DiffCollection.new([]) }
|
||||
|
||||
describe '#overflow?' do
|
||||
subject { super().overflow? }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
describe '#empty?' do
|
||||
subject { super().empty? }
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
describe '#size' do
|
||||
subject { super().size }
|
||||
it { is_expected.to eq(0) }
|
||||
end
|
||||
|
||||
describe '#real_size' do
|
||||
subject { super().real_size }
|
||||
it { is_expected.to eq('0')}
|
||||
end
|
||||
end
|
||||
|
||||
describe :each do
|
||||
context 'when diff are too large' do
|
||||
let(:collection) do
|
||||
Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }])
|
||||
end
|
||||
|
||||
it 'yields Diff instances even when they are too large' do
|
||||
expect { |b| collection.each(&b) }.
|
||||
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
|
||||
end
|
||||
|
||||
it 'prunes diffs that are too large' do
|
||||
diff = nil
|
||||
|
||||
collection.each do |d|
|
||||
diff = d
|
||||
end
|
||||
|
||||
expect(diff.diff).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff is quite large will collapse by default' do
|
||||
let(:iterator) { [{ diff: 'a' * 20480 }] }
|
||||
|
||||
context 'when no collapse is set' do
|
||||
let(:no_collapse) { true }
|
||||
|
||||
it 'yields Diff instances even when they are quite big' do
|
||||
expect { |b| subject.each(&b) }.
|
||||
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
|
||||
end
|
||||
|
||||
it 'does not prune diffs' do
|
||||
diff = nil
|
||||
|
||||
subject.each do |d|
|
||||
diff = d
|
||||
end
|
||||
|
||||
expect(diff.diff).not_to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no collapse is unset' do
|
||||
let(:no_collapse) { false }
|
||||
|
||||
it 'yields Diff instances even when they are quite big' do
|
||||
expect { |b| subject.each(&b) }.
|
||||
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
|
||||
end
|
||||
|
||||
it 'prunes diffs that are quite big' do
|
||||
diff = nil
|
||||
|
||||
subject.each do |d|
|
||||
diff = d
|
||||
end
|
||||
|
||||
expect(diff.diff).to eq('')
|
||||
end
|
||||
|
||||
context 'when go over safe limits on files' do
|
||||
let(:iterator) { [ fake_diff(1, 1) ] * 4 }
|
||||
|
||||
before(:each) do
|
||||
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
|
||||
end
|
||||
|
||||
it 'prunes diffs by default even little ones' do
|
||||
subject.each_with_index do |d, i|
|
||||
if i < 2
|
||||
expect(d.diff).not_to eq('')
|
||||
else # 90 lines
|
||||
expect(d.diff).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when go over safe limits on lines' do
|
||||
let(:iterator) do
|
||||
[
|
||||
fake_diff(1, 45),
|
||||
fake_diff(1, 45),
|
||||
fake_diff(1, 20480),
|
||||
fake_diff(1, 1)
|
||||
]
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
|
||||
end
|
||||
|
||||
it 'prunes diffs by default even little ones' do
|
||||
subject.each_with_index do |d, i|
|
||||
if i < 2
|
||||
expect(d.diff).not_to eq('')
|
||||
else # 90 lines
|
||||
expect(d.diff).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when go over safe limits on bytes' do
|
||||
let(:iterator) do
|
||||
[
|
||||
fake_diff(1, 45),
|
||||
fake_diff(1, 45),
|
||||
fake_diff(1, 20480),
|
||||
fake_diff(1, 1)
|
||||
]
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
|
||||
end
|
||||
|
||||
it 'prunes diffs by default even little ones' do
|
||||
subject.each_with_index do |d, i|
|
||||
if i < 2
|
||||
expect(d.diff).not_to eq('')
|
||||
else # > 80 bytes
|
||||
expect(d.diff).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when limiting is disabled' do
|
||||
let(:all_diffs) { true }
|
||||
|
||||
it 'yields Diff instances even when they are quite big' do
|
||||
expect { |b| subject.each(&b) }.
|
||||
to yield_with_args(an_instance_of(Gitlab::Git::Diff))
|
||||
end
|
||||
|
||||
it 'does not prune diffs' do
|
||||
diff = nil
|
||||
|
||||
subject.each do |d|
|
||||
diff = d
|
||||
end
|
||||
|
||||
expect(diff.diff).not_to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fake_diff(line_length, line_count)
|
||||
{ 'diff' => "#{'a' * line_length}\n" * line_count }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Diff, seed_helper: true do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
|
||||
before do
|
||||
@raw_diff_hash = {
|
||||
diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""),
|
||||
--- a/.gitmodules
|
||||
+++ b/.gitmodules
|
||||
@@ -4,3 +4,6 @@
|
||||
[submodule "gitlab-shell"]
|
||||
\tpath = gitlab-shell
|
||||
\turl = https://github.com/gitlabhq/gitlab-shell.git
|
||||
+[submodule "gitlab-grack"]
|
||||
+ path = gitlab-grack
|
||||
+ url = https://gitlab.com/gitlab-org/gitlab-grack.git
|
||||
|
||||
EOT
|
||||
new_path: ".gitmodules",
|
||||
old_path: ".gitmodules",
|
||||
a_mode: '100644',
|
||||
b_mode: '100644',
|
||||
new_file: false,
|
||||
renamed_file: false,
|
||||
deleted_file: false,
|
||||
too_large: false
|
||||
}
|
||||
|
||||
@rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths:
|
||||
[".gitmodules"]).patches.first
|
||||
end
|
||||
|
||||
describe '.new' do
|
||||
context 'using a Hash' do
|
||||
context 'with a small diff' do
|
||||
let(:diff) { described_class.new(@raw_diff_hash) }
|
||||
|
||||
it 'initializes the diff' do
|
||||
expect(diff.to_hash).to eq(@raw_diff_hash)
|
||||
end
|
||||
|
||||
it 'does not prune the diff' do
|
||||
expect(diff).not_to be_too_large
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a diff that is too large' do
|
||||
it 'prunes the diff' do
|
||||
diff = described_class.new(diff: 'a' * 204800)
|
||||
|
||||
expect(diff.diff).to be_empty
|
||||
expect(diff).to be_too_large
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a Rugged::Patch' do
|
||||
context 'with a small diff' do
|
||||
let(:diff) { described_class.new(@rugged_diff) }
|
||||
|
||||
it 'initializes the diff' do
|
||||
expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil))
|
||||
end
|
||||
|
||||
it 'does not prune the diff' do
|
||||
expect(diff).not_to be_too_large
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a diff that is too large' do
|
||||
it 'prunes the diff' do
|
||||
expect_any_instance_of(String).to receive(:bytesize).
|
||||
and_return(1024 * 1024 * 1024)
|
||||
|
||||
diff = described_class.new(@rugged_diff)
|
||||
|
||||
expect(diff.diff).to be_empty
|
||||
expect(diff).to be_too_large
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a collapsable diff that is too large' do
|
||||
before do
|
||||
# The patch total size is 200, with lines between 21 and 54.
|
||||
# This is a quick-and-dirty way to test this. Ideally, a new patch is
|
||||
# added to the test repo with a size that falls between the real limits.
|
||||
stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150)
|
||||
stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100)
|
||||
end
|
||||
|
||||
it 'prunes the diff as a large diff instead of as a collapsed diff' do
|
||||
diff = described_class.new(@rugged_diff, collapse: true)
|
||||
|
||||
expect(diff.diff).to be_empty
|
||||
expect(diff).to be_too_large
|
||||
expect(diff).not_to be_collapsed
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a large binary diff' do
|
||||
it 'does not prune the diff' do
|
||||
expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?).
|
||||
and_return(true)
|
||||
|
||||
diff = described_class.new(@rugged_diff)
|
||||
|
||||
expect(diff.diff).not_to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'straight diffs' do
|
||||
let(:options) { { straight: true } }
|
||||
let(:diffs) { described_class.between(repository, 'feature', 'master', options) }
|
||||
|
||||
it 'has the correct size' do
|
||||
expect(diffs.size).to eq(24)
|
||||
end
|
||||
|
||||
context 'diff' do
|
||||
it 'is an instance of Diff' do
|
||||
expect(diffs.first).to be_kind_of(described_class)
|
||||
end
|
||||
|
||||
it 'has the correct new_path' do
|
||||
expect(diffs.first.new_path).to eq('.DS_Store')
|
||||
end
|
||||
|
||||
it 'has the correct diff' do
|
||||
expect(diffs.first.diff).to include('Binary files /dev/null and b/.DS_Store differ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.between' do
|
||||
let(:diffs) { described_class.between(repository, 'feature', 'master') }
|
||||
subject { diffs }
|
||||
|
||||
it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
|
||||
|
||||
describe '#size' do
|
||||
subject { super().size }
|
||||
|
||||
it { is_expected.to eq(1) }
|
||||
end
|
||||
|
||||
context 'diff' do
|
||||
subject { diffs.first }
|
||||
|
||||
it { is_expected.to be_kind_of described_class }
|
||||
|
||||
describe '#new_path' do
|
||||
subject { super().new_path }
|
||||
|
||||
it { is_expected.to eq('files/ruby/feature.rb') }
|
||||
end
|
||||
|
||||
describe '#diff' do
|
||||
subject { super().diff }
|
||||
|
||||
it { is_expected.to include '+class Feature' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.filter_diff_options' do
|
||||
let(:options) { { max_size: 100, invalid_opt: true } }
|
||||
|
||||
context "without default options" do
|
||||
let(:filtered_options) { described_class.filter_diff_options(options) }
|
||||
|
||||
it "should filter invalid options" do
|
||||
expect(filtered_options).not_to have_key(:invalid_opt)
|
||||
end
|
||||
end
|
||||
|
||||
context "with default options" do
|
||||
let(:filtered_options) do
|
||||
default_options = { max_size: 5, bad_opt: 1, ignore_whitespace: true }
|
||||
described_class.filter_diff_options(options, default_options)
|
||||
end
|
||||
|
||||
it "should filter invalid options" do
|
||||
expect(filtered_options).not_to have_key(:invalid_opt)
|
||||
expect(filtered_options).not_to have_key(:bad_opt)
|
||||
end
|
||||
|
||||
it "should merge with default options" do
|
||||
expect(filtered_options).to have_key(:ignore_whitespace)
|
||||
end
|
||||
|
||||
it "should override default options" do
|
||||
expect(filtered_options).to have_key(:max_size)
|
||||
expect(filtered_options[:max_size]).to eq(100)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#submodule?' do
|
||||
before do
|
||||
commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
|
||||
@diffs = commit.parents[0].diff(commit).patches
|
||||
end
|
||||
|
||||
it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) }
|
||||
it { expect(described_class.new(@diffs[1]).submodule?).to eq(true) }
|
||||
end
|
||||
|
||||
describe '#line_count' do
|
||||
it 'returns the correct number of lines' do
|
||||
diff = described_class.new(@rugged_diff)
|
||||
|
||||
expect(diff.line_count).to eq(9)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#too_large?' do
|
||||
it 'returns true for a diff that is too large' do
|
||||
diff = described_class.new(diff: 'a' * 204800)
|
||||
|
||||
expect(diff.too_large?).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a diff that is small enough' do
|
||||
diff = described_class.new(diff: 'a')
|
||||
|
||||
expect(diff.too_large?).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns true for a diff that was explicitly marked as being too large' do
|
||||
diff = described_class.new(diff: 'a')
|
||||
|
||||
diff.prune_large_diff!
|
||||
|
||||
expect(diff.too_large?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#collapsed?' do
|
||||
it 'returns false by default even on quite big diff' do
|
||||
diff = described_class.new(diff: 'a' * 20480)
|
||||
|
||||
expect(diff).not_to be_collapsed
|
||||
end
|
||||
|
||||
it 'returns false by default for a diff that is small enough' do
|
||||
diff = described_class.new(diff: 'a')
|
||||
|
||||
expect(diff).not_to be_collapsed
|
||||
end
|
||||
|
||||
it 'returns true for a diff that was explicitly marked as being collapsed' do
|
||||
diff = described_class.new(diff: 'a')
|
||||
|
||||
diff.prune_collapsed_diff!
|
||||
|
||||
expect(diff).to be_collapsed
|
||||
end
|
||||
end
|
||||
|
||||
describe '#collapsible?' do
|
||||
it 'returns true for a diff that is quite large' do
|
||||
diff = described_class.new(diff: 'a' * 20480)
|
||||
|
||||
expect(diff).to be_collapsible
|
||||
end
|
||||
|
||||
it 'returns false for a diff that is small enough' do
|
||||
diff = described_class.new(diff: 'a')
|
||||
|
||||
expect(diff).not_to be_collapsible
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prune_collapsed_diff!' do
|
||||
it 'prunes the diff' do
|
||||
diff = described_class.new(diff: "foo\nbar")
|
||||
|
||||
diff.prune_collapsed_diff!
|
||||
|
||||
expect(diff.diff).to eq('')
|
||||
expect(diff.line_count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::EncodingHelper do
|
||||
let(:ext_class) { Class.new { extend EncodingHelper } }
|
||||
let(:binary_string) { File.join(SEED_REPOSITORY_PATH, 'gitlab_logo.png') }
|
||||
|
||||
describe '#encode!' do
|
||||
[
|
||||
[
|
||||
'leaves ascii only string as is',
|
||||
'ascii only string',
|
||||
'ascii only string'
|
||||
],
|
||||
[
|
||||
'leaves valid utf8 string as is',
|
||||
'multibyte string №∑∉',
|
||||
'multibyte string №∑∉'
|
||||
],
|
||||
[
|
||||
'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.',
|
||||
"mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'),
|
||||
"mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ",
|
||||
],
|
||||
].each do |description, test_string, xpect|
|
||||
it description do
|
||||
expect(ext_class.encode!(test_string)).to eq(xpect)
|
||||
end
|
||||
end
|
||||
|
||||
it 'leaves binary string as is' do
|
||||
expect(ext_class.encode!(binary_string)).to eq(binary_string)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#encode_utf8' do
|
||||
[
|
||||
[
|
||||
"encodes valid utf8 encoded string to utf8",
|
||||
"λ, λ, λ".encode("UTF-8"),
|
||||
"λ, λ, λ".encode("UTF-8"),
|
||||
],
|
||||
[
|
||||
"encodes valid ASCII-8BIT encoded string to utf8",
|
||||
"ascii only".encode("ASCII-8BIT"),
|
||||
"ascii only".encode("UTF-8"),
|
||||
],
|
||||
[
|
||||
"encodes valid ISO-8859-1 encoded string to utf8",
|
||||
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"),
|
||||
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"),
|
||||
],
|
||||
].each do |description, test_string, xpect|
|
||||
it description do
|
||||
r = ext_class.encode_utf8(test_string.force_encoding('UTF-8'))
|
||||
expect(r).to eq(xpect)
|
||||
expect(r.encoding.name).to eq('UTF-8')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clean' do
|
||||
[
|
||||
[
|
||||
'leaves ascii only string as is',
|
||||
'ascii only string',
|
||||
'ascii only string'
|
||||
],
|
||||
[
|
||||
'leaves valid utf8 string as is',
|
||||
'multibyte string №∑∉',
|
||||
'multibyte string №∑∉'
|
||||
],
|
||||
[
|
||||
'removes invalid bytes from ASCII-8bit encoded multibyte string.',
|
||||
"Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'),
|
||||
"Lorem ipsum\n dolor sit amet, xyàyùabcdùefg",
|
||||
],
|
||||
].each do |description, test_string, xpect|
|
||||
it description do
|
||||
expect(ext_class.encode!(test_string)).to eq(xpect)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Tag, seed_helper: true do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
|
||||
describe 'first tag' do
|
||||
let(:tag) { repository.tags.first }
|
||||
|
||||
it { expect(tag.name).to eq("v1.0.0") }
|
||||
it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") }
|
||||
it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") }
|
||||
it { expect(tag.message).to eq("Release") }
|
||||
end
|
||||
|
||||
describe 'last tag' do
|
||||
let(:tag) { repository.tags.last }
|
||||
|
||||
it { expect(tag.name).to eq("v1.2.1") }
|
||||
it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") }
|
||||
it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") }
|
||||
it { expect(tag.message).to eq("Version 1.2.1") }
|
||||
end
|
||||
|
||||
it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) }
|
||||
end
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Gitlab::Git::Tree, seed_helper: true do
|
||||
context :repo do
|
||||
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
|
||||
let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
|
||||
|
||||
it { expect(tree).to be_kind_of Array }
|
||||
it { expect(tree.empty?).to be_falsey }
|
||||
it { expect(tree.select(&:dir?).size).to eq(2) }
|
||||
it { expect(tree.select(&:file?).size).to eq(10) }
|
||||
it { expect(tree.select(&:submodule?).size).to eq(2) }
|
||||
|
||||
describe :dir do
|
||||
let(:dir) { tree.select(&:dir?).first }
|
||||
|
||||
it { expect(dir).to be_kind_of Gitlab::Git::Tree }
|
||||
it { expect(dir.id).to eq('3c122d2b7830eca25235131070602575cf8b41a1') }
|
||||
it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) }
|
||||
it { expect(dir.name).to eq('encoding') }
|
||||
it { expect(dir.path).to eq('encoding') }
|
||||
|
||||
context :subdir do
|
||||
let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first }
|
||||
|
||||
it { expect(subdir).to be_kind_of Gitlab::Git::Tree }
|
||||
it { expect(subdir.id).to eq('a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba') }
|
||||
it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) }
|
||||
it { expect(subdir.name).to eq('html') }
|
||||
it { expect(subdir.path).to eq('files/html') }
|
||||
end
|
||||
|
||||
context :subdir_file do
|
||||
let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
|
||||
|
||||
it { expect(subdir_file).to be_kind_of Gitlab::Git::Tree }
|
||||
it { expect(subdir_file.id).to eq('7e3e39ebb9b2bf433b4ad17313770fbe4051649c') }
|
||||
it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) }
|
||||
it { expect(subdir_file.name).to eq('popen.rb') }
|
||||
it { expect(subdir_file.path).to eq('files/ruby/popen.rb') }
|
||||
end
|
||||
end
|
||||
|
||||
describe :file do
|
||||
let(:file) { tree.select(&:file?).first }
|
||||
|
||||
it { expect(file).to be_kind_of Gitlab::Git::Tree }
|
||||
it { expect(file.id).to eq('dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82') }
|
||||
it { expect(file.commit_id).to eq(SeedRepo::Commit::ID) }
|
||||
it { expect(file.name).to eq('.gitignore') }
|
||||
end
|
||||
|
||||
describe :readme do
|
||||
let(:file) { tree.select(&:readme?).first }
|
||||
|
||||
it { expect(file).to be_kind_of Gitlab::Git::Tree }
|
||||
it { expect(file.name).to eq('README.md') }
|
||||
end
|
||||
|
||||
describe :contributing do
|
||||
let(:file) { tree.select(&:contributing?).first }
|
||||
|
||||
it { expect(file).to be_kind_of Gitlab::Git::Tree }
|
||||
it { expect(file.name).to eq('CONTRIBUTING.md') }
|
||||
end
|
||||
|
||||
describe :submodule do
|
||||
let(:submodule) { tree.select(&:submodule?).first }
|
||||
|
||||
it { expect(submodule).to be_kind_of Gitlab::Git::Tree }
|
||||
it { expect(submodule.id).to eq('79bceae69cb5750d6567b223597999bfa91cb3b9') }
|
||||
it { expect(submodule.commit_id).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
|
||||
it { expect(submodule.name).to eq('gitlab-shell') }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Git::Util do
|
||||
describe :count_lines do
|
||||
[
|
||||
["", 0],
|
||||
["foo", 1],
|
||||
["foo\n", 1],
|
||||
["foo\n\n", 2],
|
||||
].each do |string, line_count|
|
||||
it "counts #{line_count} lines in #{string.inspect}" do
|
||||
expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
RSpec::Matchers.define :be_valid_commit do
|
||||
match do |actual|
|
||||
actual &&
|
||||
actual.id == SeedRepo::Commit::ID &&
|
||||
actual.message == SeedRepo::Commit::MESSAGE &&
|
||||
actual.author_name == SeedRepo::Commit::AUTHOR_FULL_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# This file is specific to specs in spec/lib/gitlab/git/
|
||||
|
||||
SEED_REPOSITORY_PATH = File.expand_path('../../tmp/repositories', __dir__)
|
||||
TEST_REPO_PATH = File.join(SEED_REPOSITORY_PATH, 'gitlab-git-test.git')
|
||||
TEST_NORMAL_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "not-bare-repo.git")
|
||||
TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git")
|
||||
TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git")
|
||||
|
||||
module SeedHelper
|
||||
GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git"
|
||||
|
||||
def ensure_seeds
|
||||
if File.exist?(SEED_REPOSITORY_PATH)
|
||||
FileUtils.rm_r(SEED_REPOSITORY_PATH)
|
||||
end
|
||||
|
||||
FileUtils.mkdir_p(SEED_REPOSITORY_PATH)
|
||||
|
||||
create_bare_seeds
|
||||
create_normal_seeds
|
||||
create_mutable_seeds
|
||||
create_broken_seeds
|
||||
create_git_attributes
|
||||
create_invalid_git_attributes
|
||||
end
|
||||
|
||||
def create_bare_seeds
|
||||
system(git_env, *%W(git clone --bare #{GITLAB_URL}),
|
||||
chdir: SEED_REPOSITORY_PATH,
|
||||
out: '/dev/null',
|
||||
err: '/dev/null')
|
||||
end
|
||||
|
||||
def create_normal_seeds
|
||||
system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
|
||||
out: '/dev/null',
|
||||
err: '/dev/null')
|
||||
end
|
||||
|
||||
def create_mutable_seeds
|
||||
system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
|
||||
out: '/dev/null',
|
||||
err: '/dev/null')
|
||||
|
||||
system(git_env, *%w(git branch -t feature origin/feature),
|
||||
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
|
||||
|
||||
system(git_env, *%W(git remote add expendable #{GITLAB_URL}),
|
||||
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
|
||||
end
|
||||
|
||||
def create_broken_seeds
|
||||
system(git_env, *%W(git clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
|
||||
out: '/dev/null',
|
||||
err: '/dev/null')
|
||||
|
||||
refs_path = File.join(TEST_BROKEN_REPO_PATH, 'refs')
|
||||
|
||||
FileUtils.rm_r(refs_path)
|
||||
end
|
||||
|
||||
def create_git_attributes
|
||||
dir = File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git', 'info')
|
||||
|
||||
FileUtils.mkdir_p(dir)
|
||||
|
||||
File.open(File.join(dir, 'attributes'), 'w') do |handle|
|
||||
handle.write <<-EOF.strip
|
||||
# This is a comment, it should be ignored.
|
||||
|
||||
*.txt text
|
||||
*.jpg -text
|
||||
*.sh eol=lf gitlab-language=shell
|
||||
*.haml.* gitlab-language=haml
|
||||
foo/bar.* foo
|
||||
*.cgi key=value?p1=v1&p2=v2
|
||||
/*.png gitlab-language=png
|
||||
*.binary binary
|
||||
|
||||
# This uses a tab instead of spaces to ensure the parser also supports this.
|
||||
*.md\tgitlab-language=markdown
|
||||
bla/bla.txt
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
||||
def create_invalid_git_attributes
|
||||
dir = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git', 'info')
|
||||
|
||||
FileUtils.mkdir_p(dir)
|
||||
|
||||
enc = Encoding::UTF_16
|
||||
|
||||
File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle|
|
||||
handle.write('# hello'.encode(enc))
|
||||
end
|
||||
end
|
||||
|
||||
# Prevent developer git configurations from being persisted to test
|
||||
# repositories
|
||||
def git_env
|
||||
{ 'GIT_TEMPLATE_DIR' => '' }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include SeedHelper, :seed_helper
|
||||
|
||||
config.before(:all, :seed_helper) do
|
||||
ensure_seeds
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# Seed repo:
|
||||
# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
|
||||
# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
|
||||
# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
|
||||
# b0e52af38d7ea43cf41d8a6f2471351ac036d6c9 Empty commit
|
||||
# 40f4a7a617393735a95a0bb67b08385bc1e7c66d Add ISO-8859-encoded file
|
||||
# 66028349a123e695b589e09a36634d976edcc5e8 Merge branch 'add-comments-to-gitmodules' into 'master'
|
||||
# de5714f34c4e34f1d50b9a61a2e6c9132fe2b5fd Add comments to the end of .gitmodules to test parsing
|
||||
# fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 Merge branch 'add-files' into 'master'
|
||||
# eb49186cfa5c4338011f5f590fac11bd66c5c631 Add submodules nested deeper than the root
|
||||
# 18d9c205d0d22fdf62bc2f899443b83aafbf941f Add executables and links files
|
||||
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
|
||||
# 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files
|
||||
# 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules
|
||||
# d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files
|
||||
# c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files
|
||||
# ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added
|
||||
# 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified
|
||||
# 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image
|
||||
# 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added
|
||||
# 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more
|
||||
# cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule
|
||||
# 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide
|
||||
# 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit
|
||||
|
||||
module SeedRepo
|
||||
module BigCommit
|
||||
ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e"
|
||||
PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
|
||||
MESSAGE = "Files, encoding and much more"
|
||||
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
|
||||
FILES_COUNT = 2
|
||||
end
|
||||
|
||||
module Commit
|
||||
ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
|
||||
PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
|
||||
MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
|
||||
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
|
||||
FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"]
|
||||
FILES_COUNT = 2
|
||||
C_FILE_PATH = "files/ruby"
|
||||
C_FILES = ["popen.rb", "regex.rb", "version_info.rb"]
|
||||
BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
|
||||
BLOB_FILE_PATH = "app/views/keys/show.html.haml"
|
||||
end
|
||||
|
||||
module EmptyCommit
|
||||
ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9"
|
||||
PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
|
||||
MESSAGE = "Empty commit"
|
||||
AUTHOR_FULL_NAME = "Rémy Coutable"
|
||||
FILES = []
|
||||
FILES_COUNT = FILES.count
|
||||
end
|
||||
|
||||
module EncodingCommit
|
||||
ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
|
||||
PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8"
|
||||
MESSAGE = "Add ISO-8859-encoded file"
|
||||
AUTHOR_FULL_NAME = "Stan Hu"
|
||||
FILES = ["encoding/iso8859.txt"]
|
||||
FILES_COUNT = FILES.count
|
||||
end
|
||||
|
||||
module FirstCommit
|
||||
ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"
|
||||
PARENT_ID = nil
|
||||
MESSAGE = "Initial commit"
|
||||
AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
|
||||
FILES = ["LICENSE", ".gitignore", "README.md"]
|
||||
FILES_COUNT = 3
|
||||
end
|
||||
|
||||
module LastCommit
|
||||
ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6"
|
||||
PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9"
|
||||
MESSAGE = "Merge branch 'master' into 'master'"
|
||||
AUTHOR_FULL_NAME = "Stan Hu"
|
||||
FILES = ["bin/executable"]
|
||||
FILES_COUNT = FILES.count
|
||||
end
|
||||
|
||||
module Repo
|
||||
HEAD = "master"
|
||||
BRANCHES = %w[
|
||||
feature
|
||||
fix
|
||||
fix-blob-path
|
||||
fix-existing-submodule-dir
|
||||
fix-mode
|
||||
gitattributes
|
||||
gitattributes-updated
|
||||
master
|
||||
merge-test
|
||||
]
|
||||
TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1]
|
||||
end
|
||||
|
||||
module RubyBlob
|
||||
ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c"
|
||||
NAME = "popen.rb"
|
||||
CONTENT = <<-eos
|
||||
require 'fileutils'
|
||||
require 'open3'
|
||||
|
||||
module Popen
|
||||
extend self
|
||||
|
||||
def popen(cmd, path=nil)
|
||||
unless cmd.is_a?(Array)
|
||||
raise RuntimeError, "System commands must be given as an array of strings"
|
||||
end
|
||||
|
||||
path ||= Dir.pwd
|
||||
|
||||
vars = {
|
||||
"PWD" => path
|
||||
}
|
||||
|
||||
options = {
|
||||
chdir: path
|
||||
}
|
||||
|
||||
unless File.directory?(path)
|
||||
FileUtils.mkdir_p(path)
|
||||
end
|
||||
|
||||
@cmd_output = ""
|
||||
@cmd_status = 0
|
||||
|
||||
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
|
||||
@cmd_output << stdout.read
|
||||
@cmd_output << stderr.read
|
||||
@cmd_status = wait_thr.value.exitstatus
|
||||
end
|
||||
|
||||
return @cmd_output, @cmd_status
|
||||
end
|
||||
end
|
||||
eos
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue