bringit 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/bringit.rb +96 -0
- data/lib/bringit/attributes.rb +129 -0
- data/lib/bringit/blame.rb +73 -0
- data/lib/bringit/blob.rb +175 -0
- data/lib/bringit/blob_snippet.rb +30 -0
- data/lib/bringit/branch.rb +7 -0
- data/lib/bringit/cloning.rb +79 -0
- data/lib/bringit/commit.rb +331 -0
- data/lib/bringit/commit_stats.rb +24 -0
- data/lib/bringit/committing.rb +334 -0
- data/lib/bringit/committing/merge.rb +125 -0
- data/lib/bringit/compare.rb +41 -0
- data/lib/bringit/diff.rb +320 -0
- data/lib/bringit/diff_collection.rb +127 -0
- data/lib/bringit/encoding_helper.rb +56 -0
- data/lib/bringit/hook.rb +87 -0
- data/lib/bringit/index.rb +128 -0
- data/lib/bringit/path_helper.rb +14 -0
- data/lib/bringit/popen.rb +34 -0
- data/lib/bringit/pulling.rb +43 -0
- data/lib/bringit/ref.rb +56 -0
- data/lib/bringit/repository.rb +1230 -0
- data/lib/bringit/rev_list.rb +40 -0
- data/lib/bringit/tag.rb +19 -0
- data/lib/bringit/tree.rb +104 -0
- data/lib/bringit/util.rb +16 -0
- data/lib/bringit/version.rb +5 -0
- data/lib/bringit/version_info.rb +54 -0
- data/lib/bringit/wrapper.rb +136 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: be4c0173cdf310733872d3c191685be11d84ad75
|
4
|
+
data.tar.gz: '0096d31422d89baaa73df9475dac864c6166b91e'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2fb3f7d165f9ad42a5ca4880034d0dc613bd78c9ff5020e1c5bbc9439d3e12670475862b37f8dc9831f96d3fc7bae4c3cde6194756351dc6e3e0ffe63c3d0a81
|
7
|
+
data.tar.gz: '08ba4d74431dee2c8a077b26dba6667d19704daf415d8fe06db5f1ae8f0009f3b8e28956310e744bba68d8d29e4e493aea96e5ac37d6f8569ccc9f78983f935e'
|
data/lib/bringit.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Libraries
|
2
|
+
require 'ostruct'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'linguist'
|
5
|
+
require 'active_support/core_ext/hash/compact'
|
6
|
+
require 'active_support/core_ext/hash/keys'
|
7
|
+
require 'active_support/core_ext/hash/slice'
|
8
|
+
require 'active_support/core_ext/object/blank'
|
9
|
+
require 'active_support/core_ext/object/try'
|
10
|
+
require 'active_support/core_ext/module/delegation'
|
11
|
+
require 'rugged'
|
12
|
+
require "charlock_holmes"
|
13
|
+
|
14
|
+
# Bringit
|
15
|
+
require_relative "bringit/popen"
|
16
|
+
require_relative 'bringit/encoding_helper'
|
17
|
+
require_relative 'bringit/path_helper'
|
18
|
+
require_relative "bringit/blame"
|
19
|
+
require_relative "bringit/blob"
|
20
|
+
require_relative "bringit/commit"
|
21
|
+
require_relative "bringit/commit_stats"
|
22
|
+
require_relative "bringit/compare"
|
23
|
+
require_relative "bringit/diff"
|
24
|
+
require_relative "bringit/diff_collection"
|
25
|
+
require_relative "bringit/hook"
|
26
|
+
require_relative "bringit/index"
|
27
|
+
require_relative "bringit/rev_list"
|
28
|
+
require_relative "bringit/repository"
|
29
|
+
require_relative "bringit/tree"
|
30
|
+
require_relative "bringit/blob_snippet"
|
31
|
+
require_relative "bringit/ref"
|
32
|
+
require_relative "bringit/branch"
|
33
|
+
require_relative "bringit/tag"
|
34
|
+
require_relative "bringit/util"
|
35
|
+
require_relative "bringit/attributes"
|
36
|
+
require_relative "bringit/version_info"
|
37
|
+
require_relative "bringit/committing"
|
38
|
+
require_relative "bringit/cloning"
|
39
|
+
require_relative "bringit/pulling"
|
40
|
+
require_relative "bringit/wrapper"
|
41
|
+
|
42
|
+
module Bringit
|
43
|
+
BLANK_SHA = ('0' * 40).freeze
|
44
|
+
TAG_REF_PREFIX = "refs/tags/".freeze
|
45
|
+
BRANCH_REF_PREFIX = "refs/heads/".freeze
|
46
|
+
|
47
|
+
class << self
|
48
|
+
def ref_name(ref)
|
49
|
+
ref.sub(/\Arefs\/(tags|heads)\//, '')
|
50
|
+
end
|
51
|
+
|
52
|
+
def branch_name(ref)
|
53
|
+
ref = ref.to_s
|
54
|
+
if self.branch_ref?(ref)
|
55
|
+
self.ref_name(ref)
|
56
|
+
else
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def committer_hash(email:, name:)
|
62
|
+
return if email.nil? || name.nil?
|
63
|
+
|
64
|
+
{
|
65
|
+
email: email,
|
66
|
+
name: name,
|
67
|
+
time: Time.now
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def tag_name(ref)
|
72
|
+
ref = ref.to_s
|
73
|
+
if self.tag_ref?(ref)
|
74
|
+
self.ref_name(ref)
|
75
|
+
else
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def tag_ref?(ref)
|
81
|
+
ref.start_with?(TAG_REF_PREFIX)
|
82
|
+
end
|
83
|
+
|
84
|
+
def branch_ref?(ref)
|
85
|
+
ref.start_with?(BRANCH_REF_PREFIX)
|
86
|
+
end
|
87
|
+
|
88
|
+
def blank_ref?(ref)
|
89
|
+
ref == BLANK_SHA
|
90
|
+
end
|
91
|
+
|
92
|
+
def version
|
93
|
+
Bringit::VersionInfo.parse(Bringit::Popen.popen(%W(git --version)).first)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Bringit
|
2
|
+
# Class for parsing Git attribute files and extracting the attributes for
|
3
|
+
# file patterns.
|
4
|
+
#
|
5
|
+
# Unlike Rugged this parser only needs a single IO call (a call to `open`),
|
6
|
+
# vastly reducing the time spent in extracting attributes.
|
7
|
+
#
|
8
|
+
# This class _only_ supports parsing the attributes file located at
|
9
|
+
# `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
|
10
|
+
# (`.gitattributes` is copied to this particular path).
|
11
|
+
#
|
12
|
+
# Basic usage:
|
13
|
+
#
|
14
|
+
# attributes = Bringit::Attributes.new(some_repo.path)
|
15
|
+
#
|
16
|
+
# attributes.attributes('README.md') # => { "eol" => "lf }
|
17
|
+
class Attributes
|
18
|
+
# path - The path to the Git repository.
|
19
|
+
def initialize(path)
|
20
|
+
@path = File.expand_path(path)
|
21
|
+
@patterns = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns all the Git attributes for the given path.
|
25
|
+
#
|
26
|
+
# path - A path to a file for which to get the attributes.
|
27
|
+
#
|
28
|
+
# Returns a Hash.
|
29
|
+
def attributes(path)
|
30
|
+
full_path = File.join(@path, path)
|
31
|
+
|
32
|
+
patterns.each do |pattern, attrs|
|
33
|
+
return attrs if File.fnmatch?(pattern, full_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
{}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a Hash containing the file patterns and their attributes.
|
40
|
+
def patterns
|
41
|
+
@patterns ||= parse_file
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parses an attribute string.
|
45
|
+
#
|
46
|
+
# These strings can be in the following formats:
|
47
|
+
#
|
48
|
+
# text # => { "text" => true }
|
49
|
+
# -text # => { "text" => false }
|
50
|
+
# key=value # => { "key" => "value" }
|
51
|
+
#
|
52
|
+
# string - The string to parse.
|
53
|
+
#
|
54
|
+
# Returns a Hash containing the attributes and their values.
|
55
|
+
def parse_attributes(string)
|
56
|
+
values = {}
|
57
|
+
dash = '-'
|
58
|
+
equal = '='
|
59
|
+
binary = 'binary'
|
60
|
+
|
61
|
+
string.split(/\s+/).each do |chunk|
|
62
|
+
# Data such as "foo = bar" should be treated as "foo" and "bar" being
|
63
|
+
# separate boolean attributes.
|
64
|
+
next if chunk == equal
|
65
|
+
|
66
|
+
key = chunk
|
67
|
+
|
68
|
+
# Input: "-foo"
|
69
|
+
if chunk.start_with?(dash)
|
70
|
+
key = chunk.byteslice(1, chunk.length - 1)
|
71
|
+
value = false
|
72
|
+
|
73
|
+
# Input: "foo=bar"
|
74
|
+
elsif chunk.include?(equal)
|
75
|
+
key, value = chunk.split(equal, 2)
|
76
|
+
|
77
|
+
# Input: "foo"
|
78
|
+
else
|
79
|
+
value = true
|
80
|
+
end
|
81
|
+
|
82
|
+
values[key] = value
|
83
|
+
|
84
|
+
# When the "binary" option is set the "diff" option should be set to
|
85
|
+
# the inverse. If "diff" is later set it should overwrite the
|
86
|
+
# automatically set value.
|
87
|
+
values['diff'] = false if key == binary && value
|
88
|
+
end
|
89
|
+
|
90
|
+
values
|
91
|
+
end
|
92
|
+
|
93
|
+
# Iterates over every line in the attributes file.
|
94
|
+
def each_line
|
95
|
+
full_path = File.join(@path, 'info/attributes')
|
96
|
+
|
97
|
+
return unless File.exist?(full_path)
|
98
|
+
|
99
|
+
File.open(full_path, 'r') do |handle|
|
100
|
+
handle.each_line do |line|
|
101
|
+
break unless line.valid_encoding?
|
102
|
+
|
103
|
+
yield line.strip
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# Parses the Git attributes file.
|
111
|
+
def parse_file
|
112
|
+
pairs = []
|
113
|
+
comment = '#'
|
114
|
+
|
115
|
+
each_line do |line|
|
116
|
+
next if line.start_with?(comment) || line.empty?
|
117
|
+
|
118
|
+
pattern, attrs = line.split(/\s+/, 2)
|
119
|
+
|
120
|
+
parsed = attrs ? parse_attributes(attrs) : {}
|
121
|
+
|
122
|
+
pairs << [File.join(@path, pattern), parsed]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Newer entries take precedence over older entries.
|
126
|
+
pairs.reverse.to_h
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Bringit
|
2
|
+
class Blame
|
3
|
+
include Bringit::EncodingHelper
|
4
|
+
|
5
|
+
attr_reader :lines, :blames
|
6
|
+
|
7
|
+
def initialize(repository, sha, path)
|
8
|
+
@repo = repository
|
9
|
+
@sha = sha
|
10
|
+
@path = path
|
11
|
+
@lines = []
|
12
|
+
@blames = load_blame
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
@blames.each do |blame|
|
17
|
+
yield(
|
18
|
+
Bringit::Commit.new(blame.commit, @repo),
|
19
|
+
blame.line
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def load_blame
|
27
|
+
cmd = %W(git --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
|
28
|
+
# Read in binary mode to ensure ASCII-8BIT
|
29
|
+
raw_output = IO.popen(cmd, 'rb') {|io| io.read }
|
30
|
+
output = encode_utf8(raw_output)
|
31
|
+
process_raw_blame output
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_raw_blame(output)
|
35
|
+
lines, final = [], []
|
36
|
+
info, commits = {}, {}
|
37
|
+
|
38
|
+
# process the output
|
39
|
+
output.split("\n").each do |line|
|
40
|
+
if line[0, 1] == "\t"
|
41
|
+
lines << line[1, line.size]
|
42
|
+
elsif m = /^(\w{40}) (\d+) (\d+)/.match(line)
|
43
|
+
commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i
|
44
|
+
commits[commit_id] = nil unless commits.key?(commit_id)
|
45
|
+
info[lineno] = [commit_id, old_lineno]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# load all commits in single call
|
50
|
+
commits.keys.each do |key|
|
51
|
+
commits[key] = @repo.lookup(key)
|
52
|
+
end
|
53
|
+
|
54
|
+
# get it together
|
55
|
+
info.sort.each do |lineno, (commit_id, old_lineno)|
|
56
|
+
commit = commits[commit_id]
|
57
|
+
final << BlameLine.new(lineno, old_lineno, commit, lines[lineno - 1])
|
58
|
+
end
|
59
|
+
|
60
|
+
@lines = final
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class BlameLine
|
65
|
+
attr_accessor :lineno, :oldlineno, :commit, :line
|
66
|
+
def initialize(lineno, oldlineno, commit, line)
|
67
|
+
@lineno = lineno
|
68
|
+
@oldlineno = oldlineno
|
69
|
+
@commit = commit
|
70
|
+
@line = line
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/bringit/blob.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
module Bringit
|
2
|
+
class Blob
|
3
|
+
include Linguist::BlobHelper
|
4
|
+
include Bringit::EncodingHelper
|
5
|
+
|
6
|
+
# This number is the maximum amount of data that we want to display to
|
7
|
+
# the user. We load as much as we can for encoding detection
|
8
|
+
# (Linguist) and LFS pointer parsing. All other cases where we need full
|
9
|
+
# blob data should use load_all_data!.
|
10
|
+
MAX_DATA_DISPLAY_SIZE = 10485760
|
11
|
+
|
12
|
+
attr_reader :repository
|
13
|
+
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def find(repository, sha, path)
|
17
|
+
path = path&.sub(%r{\A/*}, '')
|
18
|
+
commit = repository.lookup(sha)
|
19
|
+
root_tree = commit.tree
|
20
|
+
|
21
|
+
blob_entry = find_entry_by_path(repository, root_tree.oid, path)
|
22
|
+
|
23
|
+
return nil unless blob_entry
|
24
|
+
|
25
|
+
if blob_entry[:type] == :commit
|
26
|
+
submodule_blob(repository, blob_entry, path, sha)
|
27
|
+
else
|
28
|
+
blob = repository.lookup(blob_entry[:oid])
|
29
|
+
|
30
|
+
if blob
|
31
|
+
new(repository,
|
32
|
+
id: blob.oid,
|
33
|
+
name: blob_entry[:name],
|
34
|
+
size: blob.size,
|
35
|
+
data: blob.content(MAX_DATA_DISPLAY_SIZE),
|
36
|
+
mode: blob_entry[:filemode].to_s(8),
|
37
|
+
path: Bringit::PathHelper.normalize_path(path).to_s,
|
38
|
+
commit_id: sha,
|
39
|
+
binary: blob.binary?
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def raw(repository, sha)
|
46
|
+
blob = repository.lookup(sha)
|
47
|
+
|
48
|
+
new(repository,
|
49
|
+
id: blob.oid,
|
50
|
+
size: blob.size,
|
51
|
+
data: blob.content(MAX_DATA_DISPLAY_SIZE),
|
52
|
+
binary: blob.binary?
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Recursive search of blob id by path
|
57
|
+
#
|
58
|
+
# Ex.
|
59
|
+
# blog/ # oid: 1a
|
60
|
+
# app/ # oid: 2a
|
61
|
+
# models/ # oid: 3a
|
62
|
+
# file.rb # oid: 4a
|
63
|
+
#
|
64
|
+
#
|
65
|
+
# Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a'
|
66
|
+
#
|
67
|
+
def find_entry_by_path(repository, root_id, path)
|
68
|
+
root_tree = repository.lookup(root_id)
|
69
|
+
# Strip leading slashes
|
70
|
+
path = path.sub(%r{\A/*}, '')
|
71
|
+
path_arr = path.split('/')
|
72
|
+
|
73
|
+
entry = root_tree.find do |entry|
|
74
|
+
entry[:name] == path_arr[0]
|
75
|
+
end
|
76
|
+
|
77
|
+
return nil unless entry
|
78
|
+
|
79
|
+
if path_arr.size > 1
|
80
|
+
return nil unless entry[:type] == :tree
|
81
|
+
path_arr.shift
|
82
|
+
find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
|
83
|
+
else
|
84
|
+
[:blob, :commit].include?(entry[:type]) ? entry : nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def submodule_blob(repository, blob_entry, path, sha)
|
89
|
+
path = path&.sub(%r{\A/*}, '')
|
90
|
+
new(repository,
|
91
|
+
id: blob_entry[:oid],
|
92
|
+
name: blob_entry[:name],
|
93
|
+
data: '',
|
94
|
+
path: path,
|
95
|
+
commit_id: sha,
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def initialize(repository, options)
|
101
|
+
@repository = repository
|
102
|
+
%w(id name path size data mode commit_id binary).each do |key|
|
103
|
+
self.send("#{key}=", options[key.to_sym])
|
104
|
+
end
|
105
|
+
|
106
|
+
@loaded_all_data = false
|
107
|
+
# Retain the actual size before it is encoded
|
108
|
+
@loaded_size = @data.bytesize if @data
|
109
|
+
end
|
110
|
+
|
111
|
+
def binary?
|
112
|
+
@binary.nil? ? super : @binary == true
|
113
|
+
end
|
114
|
+
|
115
|
+
def empty?
|
116
|
+
!data || data == ''
|
117
|
+
end
|
118
|
+
|
119
|
+
def data
|
120
|
+
encode! @data
|
121
|
+
end
|
122
|
+
|
123
|
+
# Load all blob data (not just the first MAX_DATA_DISPLAY_SIZE bytes) into
|
124
|
+
# memory as a Ruby string.
|
125
|
+
def load_all_data!
|
126
|
+
return if @data == '' # don't mess with submodule blobs
|
127
|
+
return @data if @loaded_all_data
|
128
|
+
|
129
|
+
@loaded_all_data = true
|
130
|
+
@data = repository.lookup(id).content
|
131
|
+
@loaded_size = @data.bytesize
|
132
|
+
end
|
133
|
+
|
134
|
+
def name
|
135
|
+
encode! @name
|
136
|
+
end
|
137
|
+
|
138
|
+
# Valid LFS object pointer is a text file consisting of
|
139
|
+
# version
|
140
|
+
# oid
|
141
|
+
# size
|
142
|
+
# see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
|
143
|
+
def lfs_pointer?
|
144
|
+
has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
|
145
|
+
end
|
146
|
+
|
147
|
+
def lfs_oid
|
148
|
+
if has_lfs_version_key?
|
149
|
+
oid = data.match(/(?<=sha256:)([0-9a-f]{64})/)
|
150
|
+
return oid[1] if oid
|
151
|
+
end
|
152
|
+
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def lfs_size
|
157
|
+
if has_lfs_version_key?
|
158
|
+
size = data.match(/(?<=size )([0-9]+)/)
|
159
|
+
return size[1] if size
|
160
|
+
end
|
161
|
+
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
|
165
|
+
def truncated?
|
166
|
+
size && (size > loaded_size)
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def has_lfs_version_key?
|
172
|
+
!empty? && text? && data.start_with?("version https://git-lfs.github.com/spec")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|