p-mongo-git 1.8.1
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/.github/stale.yml +25 -0
- data/.github/workflows/continuous_integration.yml +45 -0
- data/.gitignore +10 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +119 -0
- data/CONTRIBUTING.md +151 -0
- data/Gemfile +4 -0
- data/ISSUE_TEMPLATE.md +15 -0
- data/LICENSE +21 -0
- data/MAINTAINERS.md +14 -0
- data/PULL_REQUEST_TEMPLATE.md +9 -0
- data/README.md +344 -0
- data/RELEASING.md +62 -0
- data/Rakefile +56 -0
- data/git.gemspec +46 -0
- data/lib/git.rb +306 -0
- data/lib/git/author.rb +14 -0
- data/lib/git/base.rb +599 -0
- data/lib/git/base/factory.rb +101 -0
- data/lib/git/branch.rb +126 -0
- data/lib/git/branches.rb +71 -0
- data/lib/git/config.rb +22 -0
- data/lib/git/diff.rb +156 -0
- data/lib/git/index.rb +5 -0
- data/lib/git/lib.rb +1222 -0
- data/lib/git/log.rb +135 -0
- data/lib/git/object.rb +312 -0
- data/lib/git/path.rb +31 -0
- data/lib/git/remote.rb +36 -0
- data/lib/git/repository.rb +6 -0
- data/lib/git/stash.rb +27 -0
- data/lib/git/stashes.rb +55 -0
- data/lib/git/status.rb +199 -0
- data/lib/git/version.rb +5 -0
- data/lib/git/working_directory.rb +4 -0
- data/lib/git/worktree.rb +38 -0
- data/lib/git/worktrees.rb +47 -0
- metadata +186 -0
data/lib/git/index.rb
ADDED
data/lib/git/lib.rb
ADDED
@@ -0,0 +1,1222 @@
|
|
1
|
+
require 'rchardet'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module Git
|
6
|
+
|
7
|
+
class GitExecuteError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class Lib
|
11
|
+
|
12
|
+
@@semaphore = Mutex.new
|
13
|
+
|
14
|
+
# The path to the Git working copy. The default is '"./.git"'.
|
15
|
+
#
|
16
|
+
# @return [Pathname] the path to the Git working copy.
|
17
|
+
#
|
18
|
+
# @see [Git working tree](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefworkingtreeaworkingtree)
|
19
|
+
#
|
20
|
+
attr_reader :git_work_dir
|
21
|
+
|
22
|
+
# The path to the Git repository directory. The default is
|
23
|
+
# `"#{git_work_dir}/.git"`.
|
24
|
+
#
|
25
|
+
# @return [Pathname] the Git repository directory.
|
26
|
+
#
|
27
|
+
# @see [Git repository](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefrepositoryarepository)
|
28
|
+
#
|
29
|
+
attr_reader :git_dir
|
30
|
+
|
31
|
+
# The Git index file used to stage changes (using `git add`) before they
|
32
|
+
# are committed.
|
33
|
+
#
|
34
|
+
# @return [Pathname] the Git index file
|
35
|
+
#
|
36
|
+
# @see [Git index file](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefindexaindex)
|
37
|
+
#
|
38
|
+
attr_reader :git_index_file
|
39
|
+
|
40
|
+
# Create a new Git::Lib object
|
41
|
+
#
|
42
|
+
# @param [Git::Base, Hash] base An object that passes in values for
|
43
|
+
# @git_work_dir, @git_dir, and @git_index_file
|
44
|
+
#
|
45
|
+
# @param [Logger] logger
|
46
|
+
#
|
47
|
+
# @option base [Pathname] :working_directory
|
48
|
+
# @option base [Pathname] :repository
|
49
|
+
# @option base [Pathname] :index
|
50
|
+
#
|
51
|
+
def initialize(base = nil, logger = nil)
|
52
|
+
@git_dir = nil
|
53
|
+
@git_index_file = nil
|
54
|
+
@git_work_dir = nil
|
55
|
+
@path = nil
|
56
|
+
|
57
|
+
if base.is_a?(Git::Base)
|
58
|
+
@git_dir = base.repo.path
|
59
|
+
@git_index_file = base.index.path if base.index
|
60
|
+
@git_work_dir = base.dir.path if base.dir
|
61
|
+
elsif base.is_a?(Hash)
|
62
|
+
@git_dir = base[:repository]
|
63
|
+
@git_index_file = base[:index]
|
64
|
+
@git_work_dir = base[:working_directory]
|
65
|
+
end
|
66
|
+
@logger = logger
|
67
|
+
end
|
68
|
+
|
69
|
+
# creates or reinitializes the repository
|
70
|
+
#
|
71
|
+
# options:
|
72
|
+
# :bare
|
73
|
+
# :working_directory
|
74
|
+
#
|
75
|
+
def init(opts={})
|
76
|
+
arr_opts = []
|
77
|
+
arr_opts << '--bare' if opts[:bare]
|
78
|
+
|
79
|
+
command('init', arr_opts)
|
80
|
+
end
|
81
|
+
|
82
|
+
# tries to clone the given repo
|
83
|
+
#
|
84
|
+
# accepts options:
|
85
|
+
# :bare:: no working directory
|
86
|
+
# :branch:: name of branch to track (rather than 'master')
|
87
|
+
# :depth:: the number of commits back to pull
|
88
|
+
# :origin:: name of remote (same as remote)
|
89
|
+
# :path:: directory where the repo will be cloned
|
90
|
+
# :remote:: name of remote (rather than 'origin')
|
91
|
+
# :recursive:: after the clone is created, initialize all submodules within, using their default settings.
|
92
|
+
#
|
93
|
+
# TODO - make this work with SSH password or auth_key
|
94
|
+
#
|
95
|
+
# @return [Hash] the options to pass to {Git::Base.new}
|
96
|
+
#
|
97
|
+
def clone(repository, name, opts = {})
|
98
|
+
@path = opts[:path] || '.'
|
99
|
+
clone_dir = opts[:path] ? File.join(@path, name) : name
|
100
|
+
|
101
|
+
arr_opts = []
|
102
|
+
arr_opts << '--bare' if opts[:bare]
|
103
|
+
arr_opts << '--branch' << opts[:branch] if opts[:branch]
|
104
|
+
arr_opts << '--depth' << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0
|
105
|
+
arr_opts << '--config' << opts[:config] if opts[:config]
|
106
|
+
arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin]
|
107
|
+
arr_opts << '--recursive' if opts[:recursive]
|
108
|
+
arr_opts << "--mirror" if opts[:mirror]
|
109
|
+
|
110
|
+
arr_opts << '--'
|
111
|
+
|
112
|
+
arr_opts << repository
|
113
|
+
arr_opts << clone_dir
|
114
|
+
|
115
|
+
command('clone', arr_opts)
|
116
|
+
|
117
|
+
return_base_opts_from_clone(clone_dir, opts)
|
118
|
+
end
|
119
|
+
|
120
|
+
def return_base_opts_from_clone(clone_dir, opts)
|
121
|
+
base_opts = {}
|
122
|
+
base_opts[:repository] = clone_dir if (opts[:bare] || opts[:mirror])
|
123
|
+
base_opts[:working_directory] = clone_dir unless (opts[:bare] || opts[:mirror])
|
124
|
+
base_opts[:log] = opts[:log] if opts[:log]
|
125
|
+
base_opts
|
126
|
+
end
|
127
|
+
|
128
|
+
## READ COMMANDS ##
|
129
|
+
|
130
|
+
#
|
131
|
+
# Returns most recent tag that is reachable from a commit
|
132
|
+
#
|
133
|
+
# accepts options:
|
134
|
+
# :all
|
135
|
+
# :tags
|
136
|
+
# :contains
|
137
|
+
# :debug
|
138
|
+
# :exact_match
|
139
|
+
# :dirty
|
140
|
+
# :abbrev
|
141
|
+
# :candidates
|
142
|
+
# :long
|
143
|
+
# :always
|
144
|
+
# :math
|
145
|
+
#
|
146
|
+
# @param [String|NilClass] committish target commit sha or object name
|
147
|
+
# @param [{Symbol=>Object}] opts the given options
|
148
|
+
# @return [String] the tag name
|
149
|
+
#
|
150
|
+
def describe(committish=nil, opts={})
|
151
|
+
arr_opts = []
|
152
|
+
|
153
|
+
arr_opts << '--all' if opts[:all]
|
154
|
+
arr_opts << '--tags' if opts[:tags]
|
155
|
+
arr_opts << '--contains' if opts[:contains]
|
156
|
+
arr_opts << '--debug' if opts[:debug]
|
157
|
+
arr_opts << '--long' if opts[:long]
|
158
|
+
arr_opts << '--always' if opts[:always]
|
159
|
+
arr_opts << '--exact-match' if opts[:exact_match] || opts[:"exact-match"]
|
160
|
+
|
161
|
+
arr_opts << '--dirty' if opts[:dirty] == true
|
162
|
+
arr_opts << "--dirty=#{opts[:dirty]}" if opts[:dirty].is_a?(String)
|
163
|
+
|
164
|
+
arr_opts << "--abbrev=#{opts[:abbrev]}" if opts[:abbrev]
|
165
|
+
arr_opts << "--candidates=#{opts[:candidates]}" if opts[:candidates]
|
166
|
+
arr_opts << "--match=#{opts[:match]}" if opts[:match]
|
167
|
+
|
168
|
+
arr_opts << committish if committish
|
169
|
+
|
170
|
+
return command('describe', arr_opts)
|
171
|
+
end
|
172
|
+
|
173
|
+
def log_commits(opts={})
|
174
|
+
arr_opts = log_common_options(opts)
|
175
|
+
|
176
|
+
arr_opts << '--pretty=oneline'
|
177
|
+
|
178
|
+
arr_opts += log_path_options(opts)
|
179
|
+
|
180
|
+
command_lines('log', arr_opts).map { |l| l.split.first }
|
181
|
+
end
|
182
|
+
|
183
|
+
def full_log_commits(opts={})
|
184
|
+
arr_opts = log_common_options(opts)
|
185
|
+
|
186
|
+
arr_opts << '--pretty=raw'
|
187
|
+
arr_opts << "--skip=#{opts[:skip]}" if opts[:skip]
|
188
|
+
|
189
|
+
arr_opts += log_path_options(opts)
|
190
|
+
|
191
|
+
full_log = command_lines('log', arr_opts)
|
192
|
+
|
193
|
+
process_commit_log_data(full_log)
|
194
|
+
end
|
195
|
+
|
196
|
+
def revparse(string)
|
197
|
+
return string if string =~ /^[A-Fa-f0-9]{40}$/ # passing in a sha - just no-op it
|
198
|
+
rev = ['head', 'remotes', 'tags'].map do |d|
|
199
|
+
File.join(@git_dir, 'refs', d, string)
|
200
|
+
end.find do |path|
|
201
|
+
File.file?(path)
|
202
|
+
end
|
203
|
+
return File.read(rev).chomp if rev
|
204
|
+
command('rev-parse', string)
|
205
|
+
end
|
206
|
+
|
207
|
+
def namerev(string)
|
208
|
+
command('name-rev', string).split[1]
|
209
|
+
end
|
210
|
+
|
211
|
+
def object_type(sha)
|
212
|
+
command('cat-file', '-t', sha)
|
213
|
+
end
|
214
|
+
|
215
|
+
def object_size(sha)
|
216
|
+
command('cat-file', '-s', sha).to_i
|
217
|
+
end
|
218
|
+
|
219
|
+
# returns useful array of raw commit object data
|
220
|
+
def commit_data(sha)
|
221
|
+
sha = sha.to_s
|
222
|
+
cdata = command_lines('cat-file', 'commit', sha)
|
223
|
+
process_commit_data(cdata, sha, 0)
|
224
|
+
end
|
225
|
+
|
226
|
+
def process_commit_data(data, sha = nil, indent = 4)
|
227
|
+
hsh = {
|
228
|
+
'sha' => sha,
|
229
|
+
'message' => '',
|
230
|
+
'parent' => []
|
231
|
+
}
|
232
|
+
|
233
|
+
loop do
|
234
|
+
key, *value = data.shift.split
|
235
|
+
|
236
|
+
break if key.nil?
|
237
|
+
|
238
|
+
if key == 'parent'
|
239
|
+
hsh['parent'] << value.join(' ')
|
240
|
+
else
|
241
|
+
hsh[key] = value.join(' ')
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
hsh['message'] = data.collect {|line| line[indent..-1]}.join("\n") + "\n"
|
246
|
+
|
247
|
+
return hsh
|
248
|
+
end
|
249
|
+
|
250
|
+
def tag_data(name)
|
251
|
+
sha = sha.to_s
|
252
|
+
tdata = command_lines('cat-file', 'tag', name)
|
253
|
+
process_tag_data(tdata, name, 0)
|
254
|
+
end
|
255
|
+
|
256
|
+
def process_tag_data(data, name, indent=4)
|
257
|
+
hsh = {
|
258
|
+
'name' => name,
|
259
|
+
'message' => ''
|
260
|
+
}
|
261
|
+
|
262
|
+
loop do
|
263
|
+
key, *value = data.shift.split
|
264
|
+
|
265
|
+
break if key.nil?
|
266
|
+
|
267
|
+
hsh[key] = value.join(' ')
|
268
|
+
end
|
269
|
+
|
270
|
+
hsh['message'] = data.collect {|line| line[indent..-1]}.join("\n") + "\n"
|
271
|
+
|
272
|
+
return hsh
|
273
|
+
end
|
274
|
+
|
275
|
+
def process_commit_log_data(data)
|
276
|
+
in_message = false
|
277
|
+
|
278
|
+
hsh_array = []
|
279
|
+
|
280
|
+
hsh = nil
|
281
|
+
|
282
|
+
data.each do |line|
|
283
|
+
line = line.chomp
|
284
|
+
|
285
|
+
if line[0].nil?
|
286
|
+
in_message = !in_message
|
287
|
+
next
|
288
|
+
end
|
289
|
+
|
290
|
+
in_message = false if in_message && line[0..3] != " "
|
291
|
+
|
292
|
+
if in_message
|
293
|
+
hsh['message'] << "#{line[4..-1]}\n"
|
294
|
+
next
|
295
|
+
end
|
296
|
+
|
297
|
+
key, *value = line.split
|
298
|
+
value = value.join(' ')
|
299
|
+
|
300
|
+
case key
|
301
|
+
when 'commit'
|
302
|
+
hsh_array << hsh if hsh
|
303
|
+
hsh = {'sha' => value, 'message' => '', 'parent' => []}
|
304
|
+
when 'parent'
|
305
|
+
hsh['parent'] << value
|
306
|
+
else
|
307
|
+
hsh[key] = value
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
hsh_array << hsh if hsh
|
312
|
+
|
313
|
+
return hsh_array
|
314
|
+
end
|
315
|
+
|
316
|
+
def object_contents(sha, &block)
|
317
|
+
command('cat-file', '-p', sha, &block)
|
318
|
+
end
|
319
|
+
|
320
|
+
def ls_tree(sha)
|
321
|
+
data = {'blob' => {}, 'tree' => {}}
|
322
|
+
|
323
|
+
command_lines('ls-tree', sha).each do |line|
|
324
|
+
(info, filenm) = line.split("\t")
|
325
|
+
(mode, type, sha) = info.split
|
326
|
+
data[type][filenm] = {:mode => mode, :sha => sha}
|
327
|
+
end
|
328
|
+
|
329
|
+
data
|
330
|
+
end
|
331
|
+
|
332
|
+
def mv(file1, file2)
|
333
|
+
command_lines('mv', '--', file1, file2)
|
334
|
+
end
|
335
|
+
|
336
|
+
def full_tree(sha)
|
337
|
+
command_lines('ls-tree', '-r', sha)
|
338
|
+
end
|
339
|
+
|
340
|
+
def tree_depth(sha)
|
341
|
+
full_tree(sha).size
|
342
|
+
end
|
343
|
+
|
344
|
+
def change_head_branch(branch_name)
|
345
|
+
command('symbolic-ref', 'HEAD', "refs/heads/#{branch_name}")
|
346
|
+
end
|
347
|
+
|
348
|
+
def branches_all
|
349
|
+
arr = []
|
350
|
+
command_lines('branch', '-a').each do |b|
|
351
|
+
current = (b[0, 2] == '* ')
|
352
|
+
arr << [b.gsub('* ', '').strip, current]
|
353
|
+
end
|
354
|
+
arr
|
355
|
+
end
|
356
|
+
|
357
|
+
def worktrees_all
|
358
|
+
arr = []
|
359
|
+
directory = ''
|
360
|
+
# Output example for `worktree list --porcelain`:
|
361
|
+
# worktree /code/public/ruby-git
|
362
|
+
# HEAD 4bef5abbba073c77b4d0ccc1ffcd0ed7d48be5d4
|
363
|
+
# branch refs/heads/master
|
364
|
+
#
|
365
|
+
# worktree /tmp/worktree-1
|
366
|
+
# HEAD b8c63206f8d10f57892060375a86ae911fad356e
|
367
|
+
# detached
|
368
|
+
#
|
369
|
+
command_lines('worktree',['list', '--porcelain']).each do |w|
|
370
|
+
s = w.split("\s")
|
371
|
+
directory = s[1] if s[0] == 'worktree'
|
372
|
+
arr << [directory, s[1]] if s[0] == 'HEAD'
|
373
|
+
end
|
374
|
+
arr
|
375
|
+
end
|
376
|
+
|
377
|
+
def worktree_add(dir, commitish = nil)
|
378
|
+
return command('worktree', ['add', dir, commitish]) if !commitish.nil?
|
379
|
+
command('worktree', ['add', dir])
|
380
|
+
end
|
381
|
+
|
382
|
+
def worktree_remove(dir)
|
383
|
+
command('worktree', ['remove', dir])
|
384
|
+
end
|
385
|
+
|
386
|
+
def worktree_prune
|
387
|
+
command('worktree', ['prune'])
|
388
|
+
end
|
389
|
+
|
390
|
+
def list_files(ref_dir)
|
391
|
+
dir = File.join(@git_dir, 'refs', ref_dir)
|
392
|
+
files = []
|
393
|
+
Dir.chdir(dir) { files = Dir.glob('**/*').select { |f| File.file?(f) } } rescue nil
|
394
|
+
files
|
395
|
+
end
|
396
|
+
|
397
|
+
def branch_current
|
398
|
+
branches_all.select { |b| b[1] }.first[0] rescue nil
|
399
|
+
end
|
400
|
+
|
401
|
+
def branch_contains(commit, branch_name="")
|
402
|
+
command("branch", [branch_name, "--contains", commit])
|
403
|
+
end
|
404
|
+
|
405
|
+
# returns hash
|
406
|
+
# [tree-ish] = [[line_no, match], [line_no, match2]]
|
407
|
+
# [tree-ish] = [[line_no, match], [line_no, match2]]
|
408
|
+
def grep(string, opts = {})
|
409
|
+
opts[:object] ||= 'HEAD'
|
410
|
+
|
411
|
+
grep_opts = ['-n']
|
412
|
+
grep_opts << '-i' if opts[:ignore_case]
|
413
|
+
grep_opts << '-v' if opts[:invert_match]
|
414
|
+
grep_opts << '-e'
|
415
|
+
grep_opts << string
|
416
|
+
grep_opts << opts[:object] if opts[:object].is_a?(String)
|
417
|
+
grep_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
|
418
|
+
|
419
|
+
hsh = {}
|
420
|
+
command_lines('grep', grep_opts).each do |line|
|
421
|
+
if m = /(.*)\:(\d+)\:(.*)/.match(line)
|
422
|
+
hsh[m[1]] ||= []
|
423
|
+
hsh[m[1]] << [m[2].to_i, m[3]]
|
424
|
+
end
|
425
|
+
end
|
426
|
+
hsh
|
427
|
+
end
|
428
|
+
|
429
|
+
def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
|
430
|
+
diff_opts = ['-p']
|
431
|
+
diff_opts << obj1
|
432
|
+
diff_opts << obj2 if obj2.is_a?(String)
|
433
|
+
diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
|
434
|
+
if v = opts[:submodule]
|
435
|
+
diff_opts << "--submodule=#{v}"
|
436
|
+
end
|
437
|
+
|
438
|
+
command('diff', diff_opts)
|
439
|
+
end
|
440
|
+
|
441
|
+
def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
|
442
|
+
diff_opts = ['--numstat']
|
443
|
+
diff_opts << obj1
|
444
|
+
diff_opts << obj2 if obj2.is_a?(String)
|
445
|
+
diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
|
446
|
+
|
447
|
+
hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}}
|
448
|
+
|
449
|
+
command_lines('diff', diff_opts).each do |file|
|
450
|
+
(insertions, deletions, filename) = file.split("\t")
|
451
|
+
hsh[:total][:insertions] += insertions.to_i
|
452
|
+
hsh[:total][:deletions] += deletions.to_i
|
453
|
+
hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions])
|
454
|
+
hsh[:total][:files] += 1
|
455
|
+
hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i}
|
456
|
+
end
|
457
|
+
|
458
|
+
hsh
|
459
|
+
end
|
460
|
+
|
461
|
+
def diff_name_status(reference1 = nil, reference2 = nil, opts = {})
|
462
|
+
opts_arr = ['--name-status']
|
463
|
+
opts_arr << reference1 if reference1
|
464
|
+
opts_arr << reference2 if reference2
|
465
|
+
|
466
|
+
opts_arr << '--' << opts[:path] if opts[:path]
|
467
|
+
|
468
|
+
command_lines('diff', opts_arr).inject({}) do |memo, line|
|
469
|
+
status, path = line.split("\t")
|
470
|
+
memo[path] = status
|
471
|
+
memo
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# compares the index and the working directory
|
476
|
+
def diff_files
|
477
|
+
diff_as_hash('diff-files')
|
478
|
+
end
|
479
|
+
|
480
|
+
# compares the index and the repository
|
481
|
+
def diff_index(treeish)
|
482
|
+
diff_as_hash('diff-index', treeish)
|
483
|
+
end
|
484
|
+
|
485
|
+
def ls_files(location=nil)
|
486
|
+
location ||= '.'
|
487
|
+
hsh = {}
|
488
|
+
command_lines('ls-files', '--stage', location).each do |line|
|
489
|
+
(info, file) = line.split("\t")
|
490
|
+
(mode, sha, stage) = info.split
|
491
|
+
file = eval(file) if file =~ /^\".*\"$/ # This takes care of quoted strings returned from git
|
492
|
+
hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
|
493
|
+
end
|
494
|
+
hsh
|
495
|
+
end
|
496
|
+
|
497
|
+
def ls_remote(location=nil, opts={})
|
498
|
+
arr_opts = []
|
499
|
+
arr_opts << ['--refs'] if opts[:refs]
|
500
|
+
arr_opts << (location || '.')
|
501
|
+
|
502
|
+
Hash.new{ |h,k| h[k] = {} }.tap do |hsh|
|
503
|
+
command_lines('ls-remote', arr_opts).each do |line|
|
504
|
+
(sha, info) = line.split("\t")
|
505
|
+
(ref, type, name) = info.split('/', 3)
|
506
|
+
type ||= 'head'
|
507
|
+
type = 'branches' if type == 'heads'
|
508
|
+
value = {:ref => ref, :sha => sha}
|
509
|
+
hsh[type].update( name.nil? ? value : { name => value })
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def ignored_files
|
515
|
+
command_lines('ls-files', '--others', '-i', '--exclude-standard')
|
516
|
+
end
|
517
|
+
|
518
|
+
|
519
|
+
def config_remote(name)
|
520
|
+
hsh = {}
|
521
|
+
config_list.each do |key, value|
|
522
|
+
if /remote.#{name}/.match(key)
|
523
|
+
hsh[key.gsub("remote.#{name}.", '')] = value
|
524
|
+
end
|
525
|
+
end
|
526
|
+
hsh
|
527
|
+
end
|
528
|
+
|
529
|
+
def config_get(name)
|
530
|
+
do_get = Proc.new do |path|
|
531
|
+
command('config', '--get', name)
|
532
|
+
end
|
533
|
+
|
534
|
+
if @git_dir
|
535
|
+
Dir.chdir(@git_dir, &do_get)
|
536
|
+
else
|
537
|
+
do_get.call
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
def global_config_get(name)
|
542
|
+
command('config', '--global', '--get', name)
|
543
|
+
end
|
544
|
+
|
545
|
+
def config_list
|
546
|
+
build_list = Proc.new do |path|
|
547
|
+
parse_config_list command_lines('config', '--list')
|
548
|
+
end
|
549
|
+
|
550
|
+
if @git_dir
|
551
|
+
Dir.chdir(@git_dir, &build_list)
|
552
|
+
else
|
553
|
+
build_list.call
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
def global_config_list
|
558
|
+
parse_config_list command_lines('config', '--global', '--list')
|
559
|
+
end
|
560
|
+
|
561
|
+
def parse_config_list(lines)
|
562
|
+
hsh = {}
|
563
|
+
lines.each do |line|
|
564
|
+
(key, *values) = line.split('=')
|
565
|
+
hsh[key] = values.join('=')
|
566
|
+
end
|
567
|
+
hsh
|
568
|
+
end
|
569
|
+
|
570
|
+
def parse_config(file)
|
571
|
+
parse_config_list command_lines('config', '--list', '--file', file)
|
572
|
+
end
|
573
|
+
|
574
|
+
# Shows objects
|
575
|
+
#
|
576
|
+
# @param [String|NilClass] objectish the target object reference (nil == HEAD)
|
577
|
+
# @param [String|NilClass] path the path of the file to be shown
|
578
|
+
# @return [String] the object information
|
579
|
+
def show(objectish=nil, path=nil)
|
580
|
+
arr_opts = []
|
581
|
+
|
582
|
+
arr_opts << (path ? "#{objectish}:#{path}" : objectish)
|
583
|
+
|
584
|
+
command('show', arr_opts.compact, chomp: false)
|
585
|
+
end
|
586
|
+
|
587
|
+
## WRITE COMMANDS ##
|
588
|
+
|
589
|
+
def config_set(name, value)
|
590
|
+
command('config', name, value)
|
591
|
+
end
|
592
|
+
|
593
|
+
def global_config_set(name, value)
|
594
|
+
command('config', '--global', name, value)
|
595
|
+
end
|
596
|
+
|
597
|
+
# updates the repository index using the working directory content
|
598
|
+
#
|
599
|
+
# lib.add('path/to/file')
|
600
|
+
# lib.add(['path/to/file1','path/to/file2'])
|
601
|
+
# lib.add(:all => true)
|
602
|
+
#
|
603
|
+
# options:
|
604
|
+
# :all => true
|
605
|
+
# :force => true
|
606
|
+
#
|
607
|
+
# @param [String,Array] paths files paths to be added to the repository
|
608
|
+
# @param [Hash] options
|
609
|
+
def add(paths='.',options={})
|
610
|
+
arr_opts = []
|
611
|
+
|
612
|
+
arr_opts << '--all' if options[:all]
|
613
|
+
arr_opts << '--force' if options[:force]
|
614
|
+
|
615
|
+
arr_opts << '--'
|
616
|
+
|
617
|
+
arr_opts << paths
|
618
|
+
|
619
|
+
arr_opts.flatten!
|
620
|
+
|
621
|
+
command('add', arr_opts)
|
622
|
+
end
|
623
|
+
|
624
|
+
def remove(path = '.', opts = {})
|
625
|
+
arr_opts = ['-f'] # overrides the up-to-date check by default
|
626
|
+
arr_opts << ['-r'] if opts[:recursive]
|
627
|
+
arr_opts << ['--cached'] if opts[:cached]
|
628
|
+
arr_opts << '--'
|
629
|
+
if path.is_a?(Array)
|
630
|
+
arr_opts += path
|
631
|
+
else
|
632
|
+
arr_opts << path
|
633
|
+
end
|
634
|
+
|
635
|
+
command('rm', arr_opts)
|
636
|
+
end
|
637
|
+
|
638
|
+
# Takes the commit message with the options and executes the commit command
|
639
|
+
#
|
640
|
+
# accepts options:
|
641
|
+
# :amend
|
642
|
+
# :all
|
643
|
+
# :allow_empty
|
644
|
+
# :author
|
645
|
+
# :date
|
646
|
+
# :no_verify
|
647
|
+
# :allow_empty_message
|
648
|
+
#
|
649
|
+
# @param [String] message the commit message to be used
|
650
|
+
# @param [Hash] opts the commit options to be used
|
651
|
+
def commit(message, opts = {})
|
652
|
+
arr_opts = []
|
653
|
+
arr_opts << "--message=#{message}" if message
|
654
|
+
arr_opts << '--amend' << '--no-edit' if opts[:amend]
|
655
|
+
arr_opts << '--all' if opts[:add_all] || opts[:all]
|
656
|
+
arr_opts << '--allow-empty' if opts[:allow_empty]
|
657
|
+
arr_opts << "--author=#{opts[:author]}" if opts[:author]
|
658
|
+
arr_opts << "--date=#{opts[:date]}" if opts[:date].is_a? String
|
659
|
+
arr_opts << '--no-verify' if opts[:no_verify]
|
660
|
+
arr_opts << '--allow-empty-message' if opts[:allow_empty_message]
|
661
|
+
|
662
|
+
command('commit', arr_opts)
|
663
|
+
end
|
664
|
+
|
665
|
+
def reset(commit, opts = {})
|
666
|
+
arr_opts = []
|
667
|
+
arr_opts << '--hard' if opts[:hard]
|
668
|
+
arr_opts << commit if commit
|
669
|
+
command('reset', arr_opts)
|
670
|
+
end
|
671
|
+
|
672
|
+
def clean(opts = {})
|
673
|
+
arr_opts = []
|
674
|
+
arr_opts << '--force' if opts[:force]
|
675
|
+
arr_opts << '-d' if opts[:d]
|
676
|
+
arr_opts << '-x' if opts[:x]
|
677
|
+
|
678
|
+
command('clean', arr_opts)
|
679
|
+
end
|
680
|
+
|
681
|
+
def revert(commitish, opts = {})
|
682
|
+
# Forcing --no-edit as default since it's not an interactive session.
|
683
|
+
opts = {:no_edit => true}.merge(opts)
|
684
|
+
|
685
|
+
arr_opts = []
|
686
|
+
arr_opts << '--no-edit' if opts[:no_edit]
|
687
|
+
arr_opts << commitish
|
688
|
+
|
689
|
+
command('revert', arr_opts)
|
690
|
+
end
|
691
|
+
|
692
|
+
def apply(patch_file)
|
693
|
+
arr_opts = []
|
694
|
+
arr_opts << '--' << patch_file if patch_file
|
695
|
+
command('apply', arr_opts)
|
696
|
+
end
|
697
|
+
|
698
|
+
def apply_mail(patch_file)
|
699
|
+
arr_opts = []
|
700
|
+
arr_opts << '--' << patch_file if patch_file
|
701
|
+
command('am', arr_opts)
|
702
|
+
end
|
703
|
+
|
704
|
+
def stashes_all
|
705
|
+
arr = []
|
706
|
+
filename = File.join(@git_dir, 'logs/refs/stash')
|
707
|
+
if File.exist?(filename)
|
708
|
+
File.open(filename) do |f|
|
709
|
+
f.each_with_index do |line, i|
|
710
|
+
m = line.match(/:(.*)$/)
|
711
|
+
arr << [i, m[1].strip]
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
arr
|
716
|
+
end
|
717
|
+
|
718
|
+
def stash_save(message)
|
719
|
+
output = command('stash save', message)
|
720
|
+
output =~ /HEAD is now at/
|
721
|
+
end
|
722
|
+
|
723
|
+
def stash_apply(id = nil)
|
724
|
+
if id
|
725
|
+
command('stash apply', id)
|
726
|
+
else
|
727
|
+
command('stash apply')
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
def stash_clear
|
732
|
+
command('stash clear')
|
733
|
+
end
|
734
|
+
|
735
|
+
def stash_list
|
736
|
+
command('stash list')
|
737
|
+
end
|
738
|
+
|
739
|
+
def branch_new(branch)
|
740
|
+
command('branch', branch)
|
741
|
+
end
|
742
|
+
|
743
|
+
def branch_delete(branch)
|
744
|
+
command('branch', '-D', branch)
|
745
|
+
end
|
746
|
+
|
747
|
+
def checkout(branch, opts = {})
|
748
|
+
arr_opts = []
|
749
|
+
arr_opts << '-b' if opts[:new_branch] || opts[:b]
|
750
|
+
arr_opts << '--force' if opts[:force] || opts[:f]
|
751
|
+
arr_opts << branch
|
752
|
+
|
753
|
+
command('checkout', arr_opts)
|
754
|
+
end
|
755
|
+
|
756
|
+
def checkout_file(version, file)
|
757
|
+
arr_opts = []
|
758
|
+
arr_opts << version
|
759
|
+
arr_opts << file
|
760
|
+
command('checkout', arr_opts)
|
761
|
+
end
|
762
|
+
|
763
|
+
def merge(branch, message = nil, opts = {})
|
764
|
+
arr_opts = []
|
765
|
+
arr_opts << '--no-ff' if opts[:no_ff]
|
766
|
+
arr_opts << '-m' << message if message
|
767
|
+
arr_opts += [branch]
|
768
|
+
command('merge', arr_opts)
|
769
|
+
end
|
770
|
+
|
771
|
+
def merge_base(*args)
|
772
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
773
|
+
|
774
|
+
arg_opts = []
|
775
|
+
|
776
|
+
arg_opts << '--octopus' if opts[:octopus]
|
777
|
+
arg_opts << '--independent' if opts[:independent]
|
778
|
+
arg_opts << '--fork-point' if opts[:fork_point]
|
779
|
+
arg_opts << '--all' if opts[:all]
|
780
|
+
|
781
|
+
arg_opts += args
|
782
|
+
|
783
|
+
command('merge-base', arg_opts).lines.map(&:strip)
|
784
|
+
end
|
785
|
+
|
786
|
+
def unmerged
|
787
|
+
unmerged = []
|
788
|
+
command_lines('diff', "--cached").each do |line|
|
789
|
+
unmerged << $1 if line =~ /^\* Unmerged path (.*)/
|
790
|
+
end
|
791
|
+
unmerged
|
792
|
+
end
|
793
|
+
|
794
|
+
def conflicts # :yields: file, your, their
|
795
|
+
self.unmerged.each do |f|
|
796
|
+
your_tempfile = Tempfile.new("YOUR-#{File.basename(f)}")
|
797
|
+
your = your_tempfile.path
|
798
|
+
your_tempfile.close # free up file for git command process
|
799
|
+
command('show', ":2:#{f}", redirect: "> #{escape your}")
|
800
|
+
|
801
|
+
their_tempfile = Tempfile.new("THEIR-#{File.basename(f)}")
|
802
|
+
their = their_tempfile.path
|
803
|
+
their_tempfile.close # free up file for git command process
|
804
|
+
command('show', ":3:#{f}", redirect: "> #{escape their}")
|
805
|
+
yield(f, your, their)
|
806
|
+
end
|
807
|
+
end
|
808
|
+
|
809
|
+
def remote_add(name, url, opts = {})
|
810
|
+
arr_opts = ['add']
|
811
|
+
arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
|
812
|
+
arr_opts << '-t' << opts[:track] if opts[:track]
|
813
|
+
arr_opts << '--'
|
814
|
+
arr_opts << name
|
815
|
+
arr_opts << url
|
816
|
+
|
817
|
+
command('remote', arr_opts)
|
818
|
+
end
|
819
|
+
|
820
|
+
# accepts options:
|
821
|
+
# :push
|
822
|
+
def remote_set_url(name, url, opts = {})
|
823
|
+
arr_opts = ['set-url']
|
824
|
+
arr_opts << name
|
825
|
+
arr_opts << url
|
826
|
+
arr_opts << '--push' if opts[:push]
|
827
|
+
|
828
|
+
command('remote', arr_opts)
|
829
|
+
end
|
830
|
+
|
831
|
+
def remote_remove(name)
|
832
|
+
command('remote', 'rm', name)
|
833
|
+
end
|
834
|
+
|
835
|
+
def remotes
|
836
|
+
command_lines('remote')
|
837
|
+
end
|
838
|
+
|
839
|
+
def tags
|
840
|
+
command_lines('tag')
|
841
|
+
end
|
842
|
+
|
843
|
+
def tag(name, *opts)
|
844
|
+
target = opts[0].instance_of?(String) ? opts[0] : nil
|
845
|
+
|
846
|
+
opts = opts.last.instance_of?(Hash) ? opts.last : {}
|
847
|
+
|
848
|
+
if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message])
|
849
|
+
raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]."
|
850
|
+
end
|
851
|
+
|
852
|
+
arr_opts = []
|
853
|
+
|
854
|
+
arr_opts << '-f' if opts[:force] || opts[:f]
|
855
|
+
arr_opts << '-a' if opts[:a] || opts[:annotate]
|
856
|
+
arr_opts << '-s' if opts[:s] || opts[:sign]
|
857
|
+
arr_opts << '-d' if opts[:d] || opts[:delete]
|
858
|
+
arr_opts << name
|
859
|
+
arr_opts << target if target
|
860
|
+
|
861
|
+
if opts[:m] || opts[:message]
|
862
|
+
arr_opts << '-m' << (opts[:m] || opts[:message])
|
863
|
+
end
|
864
|
+
|
865
|
+
command('tag', arr_opts)
|
866
|
+
end
|
867
|
+
|
868
|
+
|
869
|
+
def fetch(remote, opts)
|
870
|
+
arr_opts = [remote]
|
871
|
+
arr_opts << opts[:ref] if opts[:ref]
|
872
|
+
arr_opts << '--tags' if opts[:t] || opts[:tags]
|
873
|
+
arr_opts << '--prune' if opts[:p] || opts[:prune]
|
874
|
+
arr_opts << '--unshallow' if opts[:unshallow]
|
875
|
+
|
876
|
+
command('fetch', arr_opts)
|
877
|
+
end
|
878
|
+
|
879
|
+
def push(remote, branch = 'master', opts = {})
|
880
|
+
# Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature.
|
881
|
+
opts = {:tags => opts} if [true, false].include?(opts)
|
882
|
+
|
883
|
+
arr_opts = []
|
884
|
+
arr_opts << '--mirror' if opts[:mirror]
|
885
|
+
arr_opts << '--delete' if opts[:delete]
|
886
|
+
arr_opts << '--force' if opts[:force] || opts[:f]
|
887
|
+
arr_opts << remote
|
888
|
+
|
889
|
+
if opts[:mirror]
|
890
|
+
command('push', arr_opts)
|
891
|
+
else
|
892
|
+
command('push', arr_opts + [branch])
|
893
|
+
command('push', ['--tags'] + arr_opts) if opts[:tags]
|
894
|
+
end
|
895
|
+
end
|
896
|
+
|
897
|
+
def pull(remote='origin', branch='master')
|
898
|
+
command('pull', remote, branch)
|
899
|
+
end
|
900
|
+
|
901
|
+
def tag_sha(tag_name)
|
902
|
+
head = File.join(@git_dir, 'refs', 'tags', tag_name)
|
903
|
+
return File.read(head).chomp if File.exist?(head)
|
904
|
+
|
905
|
+
command('show-ref', '--tags', '-s', tag_name)
|
906
|
+
end
|
907
|
+
|
908
|
+
def repack
|
909
|
+
command('repack', '-a', '-d')
|
910
|
+
end
|
911
|
+
|
912
|
+
def gc
|
913
|
+
command('gc', '--prune', '--aggressive', '--auto')
|
914
|
+
end
|
915
|
+
|
916
|
+
# reads a tree into the current index file
|
917
|
+
def read_tree(treeish, opts = {})
|
918
|
+
arr_opts = []
|
919
|
+
arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
|
920
|
+
arr_opts += [treeish]
|
921
|
+
command('read-tree', arr_opts)
|
922
|
+
end
|
923
|
+
|
924
|
+
def write_tree
|
925
|
+
command('write-tree')
|
926
|
+
end
|
927
|
+
|
928
|
+
def commit_tree(tree, opts = {})
|
929
|
+
opts[:message] ||= "commit tree #{tree}"
|
930
|
+
t = Tempfile.new('commit-message')
|
931
|
+
t.write(opts[:message])
|
932
|
+
t.close
|
933
|
+
|
934
|
+
arr_opts = []
|
935
|
+
arr_opts << tree
|
936
|
+
arr_opts << '-p' << opts[:parent] if opts[:parent]
|
937
|
+
arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents]
|
938
|
+
command('commit-tree', arr_opts, redirect: "< #{escape t.path}")
|
939
|
+
end
|
940
|
+
|
941
|
+
def update_ref(branch, commit)
|
942
|
+
command('update-ref', branch, commit)
|
943
|
+
end
|
944
|
+
|
945
|
+
def checkout_index(opts = {})
|
946
|
+
arr_opts = []
|
947
|
+
arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
|
948
|
+
arr_opts << "--force" if opts[:force]
|
949
|
+
arr_opts << "--all" if opts[:all]
|
950
|
+
arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
|
951
|
+
|
952
|
+
command('checkout-index', arr_opts)
|
953
|
+
end
|
954
|
+
|
955
|
+
# creates an archive file
|
956
|
+
#
|
957
|
+
# options
|
958
|
+
# :format (zip, tar)
|
959
|
+
# :prefix
|
960
|
+
# :remote
|
961
|
+
# :path
|
962
|
+
def archive(sha, file = nil, opts = {})
|
963
|
+
opts[:format] ||= 'zip'
|
964
|
+
|
965
|
+
if opts[:format] == 'tgz'
|
966
|
+
opts[:format] = 'tar'
|
967
|
+
opts[:add_gzip] = true
|
968
|
+
end
|
969
|
+
|
970
|
+
if !file
|
971
|
+
tempfile = Tempfile.new('archive')
|
972
|
+
file = tempfile.path
|
973
|
+
# delete it now, before we write to it, so that Ruby doesn't delete it
|
974
|
+
# when it finalizes the Tempfile.
|
975
|
+
tempfile.close!
|
976
|
+
end
|
977
|
+
|
978
|
+
arr_opts = []
|
979
|
+
arr_opts << "--format=#{opts[:format]}" if opts[:format]
|
980
|
+
arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
|
981
|
+
arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
|
982
|
+
arr_opts << sha
|
983
|
+
arr_opts << '--' << opts[:path] if opts[:path]
|
984
|
+
command('archive', arr_opts, redirect: " > #{escape file}")
|
985
|
+
if opts[:add_gzip]
|
986
|
+
file_content = File.read(file)
|
987
|
+
Zlib::GzipWriter.open(file) do |gz|
|
988
|
+
gz.write(file_content)
|
989
|
+
end
|
990
|
+
end
|
991
|
+
return file
|
992
|
+
end
|
993
|
+
|
994
|
+
# returns the current version of git, as an Array of Fixnums.
|
995
|
+
def current_command_version
|
996
|
+
output = command('version')
|
997
|
+
version = output[/\d+\.\d+(\.\d+)+/]
|
998
|
+
version.split('.').collect {|i| i.to_i}
|
999
|
+
end
|
1000
|
+
|
1001
|
+
def required_command_version
|
1002
|
+
[1, 6]
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def meets_required_version?
|
1006
|
+
(self.current_command_version <=> self.required_command_version) >= 0
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
|
1010
|
+
private
|
1011
|
+
|
1012
|
+
# Systen ENV variables involved in the git commands.
|
1013
|
+
#
|
1014
|
+
# @return [<String>] the names of the EVN variables involved in the git commands
|
1015
|
+
ENV_VARIABLE_NAMES = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_SSH']
|
1016
|
+
|
1017
|
+
def command_lines(cmd, *opts)
|
1018
|
+
cmd_op = command(cmd, *opts)
|
1019
|
+
if cmd_op.encoding.name != "UTF-8"
|
1020
|
+
op = cmd_op.encode("UTF-8", "binary", :invalid => :replace, :undef => :replace)
|
1021
|
+
else
|
1022
|
+
op = cmd_op
|
1023
|
+
end
|
1024
|
+
op.split("\n")
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
# Takes the current git's system ENV variables and store them.
|
1028
|
+
def store_git_system_env_variables
|
1029
|
+
@git_system_env_variables = {}
|
1030
|
+
ENV_VARIABLE_NAMES.each do |env_variable_name|
|
1031
|
+
@git_system_env_variables[env_variable_name] = ENV[env_variable_name]
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
# Takes the previously stored git's ENV variables and set them again on ENV.
|
1036
|
+
def restore_git_system_env_variables
|
1037
|
+
ENV_VARIABLE_NAMES.each do |env_variable_name|
|
1038
|
+
ENV[env_variable_name] = @git_system_env_variables[env_variable_name]
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
# Sets git's ENV variables to the custom values for the current instance.
|
1043
|
+
def set_custom_git_env_variables
|
1044
|
+
ENV['GIT_DIR'] = @git_dir
|
1045
|
+
ENV['GIT_WORK_TREE'] = @git_work_dir
|
1046
|
+
ENV['GIT_INDEX_FILE'] = @git_index_file
|
1047
|
+
ENV['GIT_SSH'] = Git::Base.config.git_ssh
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
# Runs a block inside an environment with customized ENV variables.
|
1051
|
+
# It restores the ENV after execution.
|
1052
|
+
#
|
1053
|
+
# @param [Proc] block block to be executed within the customized environment
|
1054
|
+
def with_custom_env_variables(&block)
|
1055
|
+
@@semaphore.synchronize do
|
1056
|
+
store_git_system_env_variables()
|
1057
|
+
set_custom_git_env_variables()
|
1058
|
+
return block.call()
|
1059
|
+
end
|
1060
|
+
ensure
|
1061
|
+
restore_git_system_env_variables()
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
def command(cmd, *opts, &block)
|
1065
|
+
command_opts = { chomp: true, redirect: '' }
|
1066
|
+
if opts.last.is_a?(Hash)
|
1067
|
+
command_opts.merge!(opts.pop)
|
1068
|
+
end
|
1069
|
+
command_opts.keys.each do |k|
|
1070
|
+
raise ArgumentError.new("Unsupported option: #{k}") unless [:chomp, :redirect].include?(k)
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
global_opts = []
|
1074
|
+
if cmd == 'diff'
|
1075
|
+
# For submodule diffs --work-tree alone is insufficient and -C is needed.
|
1076
|
+
global_opts << "-C" << @git_work_dir if !@git_work_dir.nil?
|
1077
|
+
end
|
1078
|
+
global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
|
1079
|
+
global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
|
1080
|
+
global_opts << ["-c", "color.ui=false"]
|
1081
|
+
|
1082
|
+
opts = [opts].flatten.map {|s| escape(s) }.join(' ')
|
1083
|
+
|
1084
|
+
global_opts = global_opts.flatten.map {|s| escape(s) }.join(' ')
|
1085
|
+
|
1086
|
+
git_cmd = "#{Git::Base.config.binary_path} #{global_opts} #{cmd} #{opts} #{command_opts[:redirect]} 2>&1"
|
1087
|
+
|
1088
|
+
output = nil
|
1089
|
+
|
1090
|
+
command_thread = nil;
|
1091
|
+
|
1092
|
+
exitstatus = nil
|
1093
|
+
|
1094
|
+
with_custom_env_variables do
|
1095
|
+
command_thread = Thread.new do
|
1096
|
+
output = run_command(git_cmd, &block)
|
1097
|
+
exitstatus = $?.exitstatus
|
1098
|
+
end
|
1099
|
+
command_thread.join
|
1100
|
+
end
|
1101
|
+
|
1102
|
+
if @logger
|
1103
|
+
@logger.info(git_cmd)
|
1104
|
+
@logger.debug(output)
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
raise Git::GitExecuteError, "#{git_cmd}:#{output}" if
|
1108
|
+
exitstatus > 1 || (exitstatus == 1 && output != '')
|
1109
|
+
|
1110
|
+
output.chomp! if output && command_opts[:chomp] && !block_given?
|
1111
|
+
|
1112
|
+
output
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
# Takes the diff command line output (as Array) and parse it into a Hash
|
1116
|
+
#
|
1117
|
+
# @param [String] diff_command the diff commadn to be used
|
1118
|
+
# @param [Array] opts the diff options to be used
|
1119
|
+
# @return [Hash] the diff as Hash
|
1120
|
+
def diff_as_hash(diff_command, opts=[])
|
1121
|
+
# update index before diffing to avoid spurious diffs
|
1122
|
+
command('status')
|
1123
|
+
command_lines(diff_command, opts).inject({}) do |memo, line|
|
1124
|
+
info, file = line.split("\t")
|
1125
|
+
mode_src, mode_dest, sha_src, sha_dest, type = info.split
|
1126
|
+
|
1127
|
+
memo[file] = {
|
1128
|
+
:mode_index => mode_dest,
|
1129
|
+
:mode_repo => mode_src.to_s[1, 7],
|
1130
|
+
:path => file,
|
1131
|
+
:sha_repo => sha_src,
|
1132
|
+
:sha_index => sha_dest,
|
1133
|
+
:type => type
|
1134
|
+
}
|
1135
|
+
|
1136
|
+
memo
|
1137
|
+
end
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
# Returns an array holding the common options for the log commands
|
1141
|
+
#
|
1142
|
+
# @param [Hash] opts the given options
|
1143
|
+
# @return [Array] the set of common options that the log command will use
|
1144
|
+
def log_common_options(opts)
|
1145
|
+
arr_opts = []
|
1146
|
+
|
1147
|
+
arr_opts << "-#{opts[:count]}" if opts[:count]
|
1148
|
+
arr_opts << "--no-color"
|
1149
|
+
arr_opts << "--cherry" if opts[:cherry]
|
1150
|
+
arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
|
1151
|
+
arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
|
1152
|
+
arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
|
1153
|
+
arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
|
1154
|
+
arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
|
1155
|
+
|
1156
|
+
arr_opts
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
# Retrurns an array holding path options for the log commands
|
1160
|
+
#
|
1161
|
+
# @param [Hash] opts the given options
|
1162
|
+
# @return [Array] the set of path options that the log command will use
|
1163
|
+
def log_path_options(opts)
|
1164
|
+
arr_opts = []
|
1165
|
+
|
1166
|
+
arr_opts << opts[:object] if opts[:object].is_a? String
|
1167
|
+
arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter]
|
1168
|
+
arr_opts
|
1169
|
+
end
|
1170
|
+
|
1171
|
+
def default_encoding
|
1172
|
+
__ENCODING__.name
|
1173
|
+
end
|
1174
|
+
|
1175
|
+
def best_guess_encoding
|
1176
|
+
# Encoding::ASCII_8BIT.name
|
1177
|
+
Encoding::UTF_8.name
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
def detected_encoding(str)
|
1181
|
+
CharDet.detect(str)['encoding'] || best_guess_encoding
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
def encoding_options
|
1185
|
+
{ invalid: :replace, undef: :replace }
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
def normalize_encoding(str)
|
1189
|
+
return str if str.valid_encoding? && str.encoding.name == default_encoding
|
1190
|
+
|
1191
|
+
return str.encode(default_encoding, str.encoding, **encoding_options) if str.valid_encoding?
|
1192
|
+
|
1193
|
+
str.encode(default_encoding, detected_encoding(str), **encoding_options)
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
def run_command(git_cmd, &block)
|
1197
|
+
return IO.popen(git_cmd, &block) if block_given?
|
1198
|
+
|
1199
|
+
`#{git_cmd}`.lines.map { |l| normalize_encoding(l) }.join
|
1200
|
+
end
|
1201
|
+
|
1202
|
+
def escape(s)
|
1203
|
+
windows_platform? ? escape_for_windows(s) : escape_for_sh(s)
|
1204
|
+
end
|
1205
|
+
|
1206
|
+
def escape_for_sh(s)
|
1207
|
+
"'#{s && s.to_s.gsub('\'','\'"\'"\'')}'"
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
def escape_for_windows(s)
|
1211
|
+
# Windows does not need single quote escaping inside double quotes
|
1212
|
+
%Q{"#{s}"}
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
def windows_platform?
|
1216
|
+
# Check if on Windows via RUBY_PLATFORM (CRuby) and RUBY_DESCRIPTION (JRuby)
|
1217
|
+
win_platform_regex = /mingw|mswin/
|
1218
|
+
RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex
|
1219
|
+
end
|
1220
|
+
|
1221
|
+
end
|
1222
|
+
end
|