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