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