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.
data/lib/git/index.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Git
2
+ class Index < Git::Path
3
+
4
+ end
5
+ end
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