bringit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -0,0 +1,24 @@
|
|
1
|
+
# Bringit::CommitStats counts the additions, deletions, and total changes
|
2
|
+
# in a commit.
|
3
|
+
module Bringit
|
4
|
+
class CommitStats
|
5
|
+
attr_reader :id, :additions, :deletions, :total
|
6
|
+
|
7
|
+
# Instantiate a CommitStats object
|
8
|
+
def initialize(commit)
|
9
|
+
@id = commit.id
|
10
|
+
@additions = 0
|
11
|
+
@deletions = 0
|
12
|
+
@total = 0
|
13
|
+
|
14
|
+
diff = commit.diff_from_parent
|
15
|
+
|
16
|
+
diff.each_patch do |p|
|
17
|
+
# TODO: Use the new Rugged convenience methods when they're released
|
18
|
+
@additions += p.stat[0]
|
19
|
+
@deletions += p.stat[1]
|
20
|
+
@total += p.changes
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,334 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "committing/merge"
|
4
|
+
|
5
|
+
module Bringit
|
6
|
+
# Methods for committing. Use all these methods only mutexed with the git
|
7
|
+
# repository as the key.
|
8
|
+
module Committing
|
9
|
+
include Bringit::Committing::Merge
|
10
|
+
|
11
|
+
class Error < StandardError; end
|
12
|
+
class InvalidPathError < Error; end
|
13
|
+
|
14
|
+
# This error is thrown when attempting to commit on a branch whose HEAD has
|
15
|
+
# changed.
|
16
|
+
class HeadChangedError < Error
|
17
|
+
attr_reader :conflicts, :options
|
18
|
+
def initialize(message, conflicts, options)
|
19
|
+
super(message)
|
20
|
+
@conflicts = conflicts
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create a file in repository and return commit sha
|
26
|
+
#
|
27
|
+
# options should contain the following structure:
|
28
|
+
# file: {
|
29
|
+
# content: 'Lorem ipsum...',
|
30
|
+
# path: 'documents/story.txt'
|
31
|
+
# },
|
32
|
+
# author: {
|
33
|
+
# email: 'user@example.com',
|
34
|
+
# name: 'Test User',
|
35
|
+
# time: Time.now # optional - default: Time.now
|
36
|
+
# },
|
37
|
+
# committer: {
|
38
|
+
# email: 'user@example.com',
|
39
|
+
# name: 'Test User',
|
40
|
+
# time: Time.now # optional - default: Time.now
|
41
|
+
# },
|
42
|
+
# commit: {
|
43
|
+
# message: 'Wow such commit',
|
44
|
+
# branch: 'master', # optional - default: 'master'
|
45
|
+
# update_ref: false # optional - default: true
|
46
|
+
# }
|
47
|
+
def create_file(options, previous_head_sha = nil)
|
48
|
+
commit_multichange(convert_options(options, :create), previous_head_sha)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Change the contents of a file in repository and return commit sha
|
52
|
+
#
|
53
|
+
# options should contain the following structure:
|
54
|
+
# file: {
|
55
|
+
# content: 'Lorem ipsum...',
|
56
|
+
# path: 'documents/story.txt'
|
57
|
+
# },
|
58
|
+
# author: {
|
59
|
+
# email: 'user@example.com',
|
60
|
+
# name: 'Test User',
|
61
|
+
# time: Time.now # optional - default: Time.now
|
62
|
+
# },
|
63
|
+
# committer: {
|
64
|
+
# email: 'user@example.com',
|
65
|
+
# name: 'Test User',
|
66
|
+
# time: Time.now # optional - default: Time.now
|
67
|
+
# },
|
68
|
+
# commit: {
|
69
|
+
# message: 'Wow such commit',
|
70
|
+
# branch: 'master', # optional - default: 'master'
|
71
|
+
# update_ref: false # optional - default: true
|
72
|
+
# }
|
73
|
+
def update_file(options, previous_head_sha = nil)
|
74
|
+
commit_multichange(convert_options(options, :update), previous_head_sha)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Change contents and path of a file in repository and return commit sha
|
78
|
+
#
|
79
|
+
# options should contain the following structure:
|
80
|
+
# file: {
|
81
|
+
# content: 'Lorem ipsum...',
|
82
|
+
# path: 'documents/story.txt',
|
83
|
+
# previous_path: 'documents/old_story.txt'
|
84
|
+
# },
|
85
|
+
# author: {
|
86
|
+
# email: 'user@example.com',
|
87
|
+
# name: 'Test User',
|
88
|
+
# time: Time.now # optional - default: Time.now
|
89
|
+
# },
|
90
|
+
# committer: {
|
91
|
+
# email: 'user@example.com',
|
92
|
+
# name: 'Test User',
|
93
|
+
# time: Time.now # optional - default: Time.now
|
94
|
+
# },
|
95
|
+
# commit: {
|
96
|
+
# message: 'Wow such commit',
|
97
|
+
# branch: 'master', # optional - default: 'master'
|
98
|
+
# update_ref: false # optional - default: true
|
99
|
+
# }
|
100
|
+
def rename_and_update_file(options, previous_head_sha = nil)
|
101
|
+
commit_multichange(convert_options(options, :rename_and_update), previous_head_sha)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Remove file from repository and return commit sha
|
105
|
+
#
|
106
|
+
# options should contain the following structure:
|
107
|
+
# file: {
|
108
|
+
# path: 'documents/story.txt'
|
109
|
+
# },
|
110
|
+
# author: {
|
111
|
+
# email: 'user@example.com',
|
112
|
+
# name: 'Test User',
|
113
|
+
# time: Time.now # optional - default: Time.now
|
114
|
+
# },
|
115
|
+
# committer: {
|
116
|
+
# email: 'user@example.com',
|
117
|
+
# name: 'Test User',
|
118
|
+
# time: Time.now # optional - default: Time.now
|
119
|
+
# },
|
120
|
+
# commit: {
|
121
|
+
# message: 'Remove FILENAME',
|
122
|
+
# branch: 'master' # optional - default: 'master'
|
123
|
+
# }
|
124
|
+
def remove_file(options, previous_head_sha = nil)
|
125
|
+
commit_multichange(convert_options(options, :remove), previous_head_sha)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Rename file from repository and return commit sha
|
129
|
+
# This does not change the file content.
|
130
|
+
#
|
131
|
+
# options should contain the following structure:
|
132
|
+
# file: {
|
133
|
+
# previous_path: 'documents/old_story.txt'
|
134
|
+
# path: 'documents/story.txt'
|
135
|
+
# },
|
136
|
+
# author: {
|
137
|
+
# email: 'user@example.com',
|
138
|
+
# name: 'Test User',
|
139
|
+
# time: Time.now # optional - default: Time.now
|
140
|
+
# },
|
141
|
+
# committer: {
|
142
|
+
# email: 'user@example.com',
|
143
|
+
# name: 'Test User',
|
144
|
+
# time: Time.now # optional - default: Time.now
|
145
|
+
# },
|
146
|
+
# commit: {
|
147
|
+
# message: 'Rename FILENAME',
|
148
|
+
# branch: 'master' # optional - default: 'master'
|
149
|
+
# }
|
150
|
+
#
|
151
|
+
def rename_file(options, previous_head_sha = nil)
|
152
|
+
commit_multichange(convert_options(options, :rename), previous_head_sha)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Create a new directory with a .gitkeep file. Creates
|
156
|
+
# all required nested directories (i.e. mkdir -p behavior)
|
157
|
+
#
|
158
|
+
# options should contain the following structure:
|
159
|
+
# author: {
|
160
|
+
# email: 'user@example.com',
|
161
|
+
# name: 'Test User',
|
162
|
+
# time: Time.now # optional - default: Time.now
|
163
|
+
# },
|
164
|
+
# committer: {
|
165
|
+
# email: 'user@example.com',
|
166
|
+
# name: 'Test User',
|
167
|
+
# time: Time.now # optional - default: Time.now
|
168
|
+
# },
|
169
|
+
# commit: {
|
170
|
+
# message: 'Wow such commit',
|
171
|
+
# branch: 'master', # optional - default: 'master'
|
172
|
+
# update_ref: false # optional - default: true
|
173
|
+
# }
|
174
|
+
def mkdir(path, options, previous_head_sha = nil)
|
175
|
+
options[:file] = {path: path}
|
176
|
+
commit_multichange(convert_options(options, :mkdir), previous_head_sha)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Apply multiple file changes to the repository
|
180
|
+
#
|
181
|
+
# options should contain the following structure:
|
182
|
+
# files: {
|
183
|
+
# [{content: 'Lorem ipsum...',
|
184
|
+
# path: 'documents/story.txt',
|
185
|
+
# action: :create},
|
186
|
+
# {content: 'New Lorem ipsum...',
|
187
|
+
# path: 'documents/old_story',
|
188
|
+
# action: :update},
|
189
|
+
# {content: 'New Lorem ipsum...',
|
190
|
+
# previous_path: 'documents/really_old_story.txt',
|
191
|
+
# path: 'documents/old_story',
|
192
|
+
# action: :rename_and_update},
|
193
|
+
# {path: 'documents/obsolet_story.txt',
|
194
|
+
# action: :remove},
|
195
|
+
# {path: 'documents/old_story',
|
196
|
+
# previus_path: 'documents/really_old_story.txt',
|
197
|
+
# action: :rename},
|
198
|
+
# {path: 'documents/secret',
|
199
|
+
# action: :mkdir}
|
200
|
+
# ]
|
201
|
+
# }
|
202
|
+
# },
|
203
|
+
# author: {
|
204
|
+
# email: 'user@example.com',
|
205
|
+
# name: 'Test User',
|
206
|
+
# time: Time.now # optional - default: Time.now
|
207
|
+
# },
|
208
|
+
# committer: {
|
209
|
+
# email: 'user@example.com',
|
210
|
+
# name: 'Test User',
|
211
|
+
# time: Time.now # optional - default: Time.now
|
212
|
+
# },
|
213
|
+
# commit: {
|
214
|
+
# message: 'Wow such commit',
|
215
|
+
# branch: 'master', # optional - default: 'master'
|
216
|
+
# update_ref: false # optional - default: true
|
217
|
+
# }
|
218
|
+
def commit_multichange(options, previous_head_sha = nil)
|
219
|
+
commit_with(options, previous_head_sha) do |index|
|
220
|
+
options[:files].each do |file|
|
221
|
+
file_options = {}
|
222
|
+
file_options[:file_path] = file[:path] if file[:path]
|
223
|
+
file_options[:content] = file[:content] if file[:content]
|
224
|
+
file_options[:encoding] = file[:encoding] if file[:encoding]
|
225
|
+
case file[:action]
|
226
|
+
when :create
|
227
|
+
index.create(file_options)
|
228
|
+
when :rename
|
229
|
+
file_options[:previous_path] = file[:previous_path]
|
230
|
+
file_options[:content] ||=
|
231
|
+
blob(options[:commit][:branch], file[:previous_path]).data
|
232
|
+
index.move(file_options)
|
233
|
+
when :update
|
234
|
+
index.update(file_options)
|
235
|
+
when :rename_and_update
|
236
|
+
previous_path = file[:previous_path]
|
237
|
+
file_options[:previous_path] = previous_path
|
238
|
+
index.move(file_options)
|
239
|
+
when :remove
|
240
|
+
index.delete(file_options)
|
241
|
+
when :mkdir
|
242
|
+
index.create_dir(file_options)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
protected
|
249
|
+
|
250
|
+
|
251
|
+
# Converts the options from a single change commit to a multi change
|
252
|
+
# commit.
|
253
|
+
def convert_options(options, action)
|
254
|
+
converted = options.dup
|
255
|
+
converted.delete(:file)
|
256
|
+
converted[:files] = [options[:file].merge(action: action)]
|
257
|
+
converted
|
258
|
+
end
|
259
|
+
|
260
|
+
def insert_defaults(options)
|
261
|
+
options[:author][:time] ||= Time.now
|
262
|
+
options[:committer][:time] ||= Time.now
|
263
|
+
options[:commit][:branch] ||= 'master'
|
264
|
+
options[:commit][:update_ref] = true if options[:commit][:update_ref].nil?
|
265
|
+
normalize_ref(options)
|
266
|
+
normalize_update_ref(options)
|
267
|
+
end
|
268
|
+
|
269
|
+
def normalize_ref(options)
|
270
|
+
return if options[:commit][:branch].start_with?('refs/')
|
271
|
+
options[:commit][:branch] = 'refs/heads/' + options[:commit][:branch]
|
272
|
+
end
|
273
|
+
|
274
|
+
def normalize_update_ref(options)
|
275
|
+
options[:commit][:update_ref] =
|
276
|
+
if options[:commit][:update_ref].nil?
|
277
|
+
true
|
278
|
+
else
|
279
|
+
options[:commit][:update_ref]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# This method does the actual committing. Use this mutexed with the git
|
284
|
+
# repository as the key.
|
285
|
+
# rubocop:disable Metrics/AbcSize
|
286
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
287
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
288
|
+
# rubocop:disable Metrics/MethodLength
|
289
|
+
def commit_with(options, previous_head_sha)
|
290
|
+
# rubocop:enable Metrics/AbcSize
|
291
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
292
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
293
|
+
# rubocop:enable Metrics/MethodLength
|
294
|
+
insert_defaults(options)
|
295
|
+
action, commit_sha = merge_if_needed(options, previous_head_sha)
|
296
|
+
return commit_sha if action == :merge_commit_created
|
297
|
+
|
298
|
+
index = Bringit::Index.new(bringit)
|
299
|
+
parents, last_commit = parents_and_last_commit(options)
|
300
|
+
index.read_tree(last_commit.tree) if last_commit
|
301
|
+
|
302
|
+
yield(index)
|
303
|
+
create_commit(index, index.write_tree, options, parents)
|
304
|
+
end
|
305
|
+
|
306
|
+
def parents_and_last_commit(options)
|
307
|
+
parents = []
|
308
|
+
last_commit = nil
|
309
|
+
unless empty?
|
310
|
+
rugged_ref = rugged.references[options[:commit][:branch]]
|
311
|
+
unless rugged_ref
|
312
|
+
raise Bringit::Repository::InvalidRef, 'Invalid branch name'
|
313
|
+
end
|
314
|
+
last_commit = rugged_ref.target
|
315
|
+
parents = [last_commit]
|
316
|
+
end
|
317
|
+
[parents, last_commit]
|
318
|
+
end
|
319
|
+
|
320
|
+
def create_commit(index, tree, options, parents)
|
321
|
+
opts = {}
|
322
|
+
opts[:tree] = tree
|
323
|
+
opts[:author] = options[:author]
|
324
|
+
opts[:committer] = options[:committer]
|
325
|
+
opts[:message] = options[:commit][:message]
|
326
|
+
opts[:parents] = parents
|
327
|
+
if options[:commit][:update_ref]
|
328
|
+
opts[:update_ref] = options[:commit][:branch]
|
329
|
+
end
|
330
|
+
|
331
|
+
Rugged::Commit.create(rugged, opts)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bringit
|
4
|
+
module Committing
|
5
|
+
# Methods for merging a commit into another if the previous_head_sha is
|
6
|
+
# an ancestor of the current HEAD of a branch.
|
7
|
+
module Merge
|
8
|
+
def merge_if_needed(options, previous_head_sha)
|
9
|
+
return [:noop, nil] unless diverged?(options, previous_head_sha)
|
10
|
+
|
11
|
+
commit_sha = merge(options, previous_head_sha)
|
12
|
+
return [:merge_commit_created, commit_sha]
|
13
|
+
end
|
14
|
+
|
15
|
+
def diverged?(options, previous_head_sha)
|
16
|
+
!previous_head_sha.nil? &&
|
17
|
+
branch_sha(options[:commit][:branch]) != previous_head_sha
|
18
|
+
end
|
19
|
+
|
20
|
+
def merge(options, previous_head_sha)
|
21
|
+
user_commit = create_user_commit(options, previous_head_sha)
|
22
|
+
base_commit = commit(options[:commit][:branch]).raw_commit
|
23
|
+
|
24
|
+
index = rugged.merge_commits(base_commit, user_commit)
|
25
|
+
|
26
|
+
if index.conflicts?
|
27
|
+
enriched_conflicts = add_merge_data(options, index)
|
28
|
+
raise_head_changed_error(enriched_conflicts, options)
|
29
|
+
end
|
30
|
+
tree_id = index.write_tree(rugged)
|
31
|
+
found_conflicts = conflicts(options, base_commit, user_commit)
|
32
|
+
if found_conflicts.any?
|
33
|
+
raise_head_changed_error(found_conflicts, options)
|
34
|
+
end
|
35
|
+
create_merging_commit(base_commit, index, tree_id, options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_merge_data(options, index)
|
39
|
+
our_label = options[:commit][:branch].sub(%r{\Arefs/heads/}, '')
|
40
|
+
index.conflicts.map do |conflict|
|
41
|
+
if conflict[:ancestor] && conflict[:ours] && conflict[:theirs]
|
42
|
+
conflict[:merge_info] =
|
43
|
+
index.merge_file(conflict[:ours][:path],
|
44
|
+
ancestor_label: 'parent',
|
45
|
+
our_label: our_label,
|
46
|
+
their_label: options[:commit][:message])
|
47
|
+
else
|
48
|
+
conflict[:merge_info] = nil
|
49
|
+
end
|
50
|
+
conflict
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def conflicts(options, base_commit, user_commit)
|
55
|
+
options[:files].map do |file|
|
56
|
+
case file[:action]
|
57
|
+
when :update
|
58
|
+
conflict_on_update(base_commit, user_commit, file[:path])
|
59
|
+
when :rename_and_update
|
60
|
+
conflict_on_update(base_commit, user_commit, file[:previous_path])
|
61
|
+
end
|
62
|
+
end.compact
|
63
|
+
end
|
64
|
+
|
65
|
+
def conflict_on_update(base_commit, user_commit, path)
|
66
|
+
base_blob = blob(base_commit.oid, path)
|
67
|
+
return nil unless base_blob.nil?
|
68
|
+
|
69
|
+
ancestor_blob = blob(base_commit.parents.first.oid, path)
|
70
|
+
user_blob = blob(user_commit.oid, path)
|
71
|
+
result = {merge_info: nil, ours: nil}
|
72
|
+
result[:ancestor] = conflict_hash(ancestor_blob, 1) if ancestor_blob
|
73
|
+
result[:theirs] = conflict_hash(user_blob, 3) if user_blob
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def conflict_hash(blob_object, stage)
|
78
|
+
{path: blob_object.path,
|
79
|
+
oid: blob_object.id,
|
80
|
+
dev: 0,
|
81
|
+
ino: 0,
|
82
|
+
mode: blob_object.mode.to_i(8),
|
83
|
+
gid: 0,
|
84
|
+
uid: 0,
|
85
|
+
file_size: 0,
|
86
|
+
valid: false,
|
87
|
+
stage: stage,
|
88
|
+
ctime: Time.at(0),
|
89
|
+
mtime: Time.at(0)}
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_user_commit(options, previous_head_sha)
|
93
|
+
with_temp_user_reference(options, previous_head_sha) do |reference|
|
94
|
+
new_options = options.dup
|
95
|
+
new_options[:commit] = options[:commit].dup
|
96
|
+
new_options[:commit][:branch] = reference.name
|
97
|
+
new_options[:commit][:update_ref] = false
|
98
|
+
commit_sha = commit_multichange(new_options)
|
99
|
+
rugged.lookup(commit_sha)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def with_temp_user_reference(options, previous_head_sha)
|
104
|
+
refname = "#{Time.now.to_f.to_s.tr('.', '')}_#{SecureRandom.hex(20)}"
|
105
|
+
full_refname = "refs/merges/user/#{refname}"
|
106
|
+
reference = rugged.references.create(full_refname, previous_head_sha)
|
107
|
+
yield(reference)
|
108
|
+
ensure
|
109
|
+
rugged.references.delete(reference)
|
110
|
+
end
|
111
|
+
|
112
|
+
def create_merging_commit(parent_commit, index, tree_id, options)
|
113
|
+
parents = [parent_commit.oid]
|
114
|
+
create_commit(index, tree_id, options, parents)
|
115
|
+
end
|
116
|
+
|
117
|
+
def raise_head_changed_error(conflicts, options)
|
118
|
+
message = <<MESSAGE
|
119
|
+
The branch has changed since editing and cannot be merged automatically.
|
120
|
+
MESSAGE
|
121
|
+
raise HeadChangedError.new(message, conflicts, options)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|