git-glimmer 1.7.0

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