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
@@ -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
|