ruby-git-fw 1.3.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/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,1014 @@
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 << "--diff-filter=ADM"
348
+ diff_opts << obj1
349
+ diff_opts << obj2 if obj2.is_a?(String)
350
+ diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
351
+
352
+ command('diff', diff_opts)
353
+ end
354
+
355
+ def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
356
+ diff_opts = ['--numstat']
357
+ diff_opts << "--diff-filter=ADM"
358
+ diff_opts << obj1
359
+ diff_opts << obj2 if obj2.is_a?(String)
360
+ diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
361
+
362
+ hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}}
363
+
364
+ command_lines('diff', diff_opts).each do |file|
365
+ (insertions, deletions, filename) = file.split("\t")
366
+ hsh[:total][:insertions] += insertions.to_i
367
+ hsh[:total][:deletions] += deletions.to_i
368
+ hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions])
369
+ hsh[:total][:files] += 1
370
+ hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i}
371
+ end
372
+
373
+ hsh
374
+ end
375
+
376
+ def diff_name_status(reference1 = nil, reference2 = nil, opts = {})
377
+ opts_arr = ['--name-status']
378
+ opts_arr << reference1 if reference1
379
+ opts_arr << reference2 if reference2
380
+
381
+ opts_arr << '--' << opts[:path] if opts[:path]
382
+
383
+ command_lines('diff', opts_arr).inject({}) do |memo, line|
384
+ status, path = line.split("\t")
385
+ memo[path] = status
386
+ memo
387
+ end
388
+ end
389
+
390
+ # compares the index and the working directory
391
+ def diff_files
392
+ diff_as_hash('diff-files')
393
+ end
394
+
395
+ # compares the index and the repository
396
+ def diff_index(treeish)
397
+ diff_as_hash('diff-index', treeish)
398
+ end
399
+
400
+ def ls_files(location=nil)
401
+ hsh = {}
402
+ command_lines('ls-files', ['--stage', location]).each do |line|
403
+ (info, file) = line.split("\t")
404
+ (mode, sha, stage) = info.split
405
+ file = eval(file) if file =~ /^\".*\"$/ # This takes care of quoted strings returned from git
406
+ hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
407
+ end
408
+ hsh
409
+ end
410
+
411
+ def ls_remote(location=nil)
412
+ location ||= '.'
413
+ Hash.new{ |h,k| h[k] = {} }.tap do |hsh|
414
+ command_lines('ls-remote', [location], false).each do |line|
415
+ (sha, info) = line.split("\t")
416
+ (ref, type, name) = info.split('/', 3)
417
+ type ||= 'head'
418
+ type = 'branches' if type == 'heads'
419
+ value = {:ref => ref, :sha => sha}
420
+ hsh[type].update( name.nil? ? value : { name => value })
421
+ end
422
+ end
423
+ end
424
+
425
+ def ignored_files
426
+ command_lines('ls-files', ['--others', '-i', '--exclude-standard'])
427
+ end
428
+
429
+
430
+ def config_remote(name)
431
+ hsh = {}
432
+ config_list.each do |key, value|
433
+ if /remote.#{name}/.match(key)
434
+ hsh[key.gsub("remote.#{name}.", '')] = value
435
+ end
436
+ end
437
+ hsh
438
+ end
439
+
440
+ def config_get(name)
441
+ do_get = lambda do |path|
442
+ command('config', ['--get', name])
443
+ end
444
+
445
+ if @git_dir
446
+ Dir.chdir(@git_dir, &do_get)
447
+ else
448
+ build_list.call
449
+ end
450
+ end
451
+
452
+ def global_config_get(name)
453
+ command('config', ['--global', '--get', name], false)
454
+ end
455
+
456
+ def config_list
457
+ build_list = lambda do |path|
458
+ parse_config_list command_lines('config', ['--list'])
459
+ end
460
+
461
+ if @git_dir
462
+ Dir.chdir(@git_dir, &build_list)
463
+ else
464
+ build_list.call
465
+ end
466
+ end
467
+
468
+ def global_config_list
469
+ parse_config_list command_lines('config', ['--global', '--list'], false)
470
+ end
471
+
472
+ def parse_config_list(lines)
473
+ hsh = {}
474
+ lines.each do |line|
475
+ (key, *values) = line.split('=')
476
+ hsh[key] = values.join('=')
477
+ end
478
+ hsh
479
+ end
480
+
481
+ def parse_config(file)
482
+ parse_config_list command_lines('config', ['--list', '--file', file], false)
483
+ end
484
+
485
+ # Shows objects
486
+ #
487
+ # @param [String|NilClass] objectish the target object reference (nil == HEAD)
488
+ # @param [String|NilClass] path the path of the file to be shown
489
+ # @return [String] the object information
490
+ def show(objectish=nil, path=nil)
491
+ arr_opts = []
492
+
493
+ arr_opts << (path ? "#{objectish}:#{path}" : objectish)
494
+
495
+ command('show', arr_opts.compact)
496
+ end
497
+
498
+ ## WRITE COMMANDS ##
499
+
500
+ def config_set(name, value)
501
+ command('config', [name, value])
502
+ end
503
+
504
+ def global_config_set(name, value)
505
+ command('config', ['--global', name, value], false)
506
+ end
507
+
508
+ # updates the repository index using the working directory content
509
+ #
510
+ # lib.add('path/to/file')
511
+ # lib.add(['path/to/file1','path/to/file2'])
512
+ # lib.add(:all => true)
513
+ #
514
+ # options:
515
+ # :all => true
516
+ # :force => true
517
+ #
518
+ # @param [String,Array] paths files paths to be added to the repository
519
+ # @param [Hash] options
520
+ def add(paths='.',options={})
521
+ arr_opts = []
522
+
523
+ arr_opts << '--all' if options[:all]
524
+ arr_opts << '--force' if options[:force]
525
+
526
+ arr_opts << '--'
527
+
528
+ arr_opts << paths
529
+
530
+ arr_opts.flatten!
531
+
532
+ command('add', arr_opts)
533
+ end
534
+
535
+ def remove(path = '.', opts = {})
536
+ arr_opts = ['-f'] # overrides the up-to-date check by default
537
+ arr_opts << ['-r'] if opts[:recursive]
538
+ arr_opts << ['--cached'] if opts[:cached]
539
+ arr_opts << '--'
540
+ if path.is_a?(Array)
541
+ arr_opts += path
542
+ else
543
+ arr_opts << path
544
+ end
545
+
546
+ command('rm', arr_opts)
547
+ end
548
+
549
+ def commit(message, opts = {})
550
+ arr_opts = []
551
+ arr_opts << "--message=#{message}" if message
552
+ arr_opts << '--amend' << '--no-edit' if opts[:amend]
553
+ arr_opts << '--all' if opts[:add_all] || opts[:all]
554
+ arr_opts << '--allow-empty' if opts[:allow_empty]
555
+ arr_opts << "--author=#{opts[:author]}" if opts[:author]
556
+
557
+ command('commit', arr_opts)
558
+ end
559
+
560
+ def reset(commit, opts = {})
561
+ arr_opts = []
562
+ arr_opts << '--hard' if opts[:hard]
563
+ arr_opts << commit if commit
564
+ command('reset', arr_opts)
565
+ end
566
+
567
+ def clean(opts = {})
568
+ arr_opts = []
569
+ arr_opts << '--force' if opts[:force]
570
+ arr_opts << '-d' if opts[:d]
571
+ arr_opts << '-x' if opts[:x]
572
+
573
+ command('clean', arr_opts)
574
+ end
575
+
576
+ def revert(commitish, opts = {})
577
+ # Forcing --no-edit as default since it's not an interactive session.
578
+ opts = {:no_edit => true}.merge(opts)
579
+
580
+ arr_opts = []
581
+ arr_opts << '--no-edit' if opts[:no_edit]
582
+ arr_opts << commitish
583
+
584
+ command('revert', arr_opts)
585
+ end
586
+
587
+ def apply(patch_file)
588
+ arr_opts = []
589
+ arr_opts << '--' << patch_file if patch_file
590
+ command('apply', arr_opts)
591
+ end
592
+
593
+ def apply_mail(patch_file)
594
+ arr_opts = []
595
+ arr_opts << '--' << patch_file if patch_file
596
+ command('am', arr_opts)
597
+ end
598
+
599
+ def stashes_all
600
+ arr = []
601
+ filename = File.join(@git_dir, 'logs/refs/stash')
602
+ if File.exist?(filename)
603
+ File.open(filename).each_with_index { |line, i|
604
+ m = line.match(/:(.*)$/)
605
+ arr << [i, m[1].strip]
606
+ }
607
+ end
608
+ arr
609
+ end
610
+
611
+ def stash_save(message)
612
+ output = command('stash save', ['--', message])
613
+ output =~ /HEAD is now at/
614
+ end
615
+
616
+ def stash_apply(id = nil)
617
+ if id
618
+ command('stash apply', [id])
619
+ else
620
+ command('stash apply')
621
+ end
622
+ end
623
+
624
+ def stash_clear
625
+ command('stash clear')
626
+ end
627
+
628
+ def stash_list
629
+ command('stash list')
630
+ end
631
+
632
+ def branch_new(branch)
633
+ command('branch', branch)
634
+ end
635
+
636
+ def branch_delete(branch)
637
+ command('branch', ['-D', branch])
638
+ end
639
+
640
+ def checkout(branch, opts = {})
641
+ arr_opts = []
642
+ arr_opts << '-b' if opts[:new_branch] || opts[:b]
643
+ arr_opts << '--force' if opts[:force] || opts[:f]
644
+ arr_opts << branch
645
+
646
+ command('checkout', arr_opts)
647
+ end
648
+
649
+ def checkout_file(version, file)
650
+ arr_opts = []
651
+ arr_opts << version
652
+ arr_opts << file
653
+ command('checkout', arr_opts)
654
+ end
655
+
656
+ def merge(branch, message = nil)
657
+ arr_opts = []
658
+ arr_opts << '-m' << message if message
659
+ arr_opts += [branch]
660
+ command('merge', arr_opts)
661
+ end
662
+
663
+ def unmerged
664
+ unmerged = []
665
+ command_lines('diff', ["--cached"]).each do |line|
666
+ unmerged << $1 if line =~ /^\* Unmerged path (.*)/
667
+ end
668
+ unmerged
669
+ end
670
+
671
+ def conflicts # :yields: file, your, their
672
+ self.unmerged.each do |f|
673
+ your = Tempfile.new("YOUR-#{File.basename(f)}").path
674
+ command('show', ":2:#{f}", true, "> #{escape your}")
675
+
676
+ their = Tempfile.new("THEIR-#{File.basename(f)}").path
677
+ command('show', ":3:#{f}", true, "> #{escape their}")
678
+ yield(f, your, their)
679
+ end
680
+ end
681
+
682
+ def remote_add(name, url, opts = {})
683
+ arr_opts = ['add']
684
+ arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
685
+ arr_opts << '-t' << opts[:track] if opts[:track]
686
+ arr_opts << '--'
687
+ arr_opts << name
688
+ arr_opts << url
689
+
690
+ command('remote', arr_opts)
691
+ end
692
+
693
+ def remote_remove(name)
694
+ command('remote', ['rm', name])
695
+ end
696
+
697
+ def remotes
698
+ command_lines('remote')
699
+ end
700
+
701
+ def tags
702
+ command_lines('tag')
703
+ end
704
+
705
+ def tag(name, *opts)
706
+ target = opts[0].instance_of?(String) ? opts[0] : nil
707
+
708
+ opts = opts.last.instance_of?(Hash) ? opts.last : {}
709
+
710
+ if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message])
711
+ raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]."
712
+ end
713
+
714
+ arr_opts = []
715
+
716
+ arr_opts << '-f' if opts[:force] || opts[:f]
717
+ arr_opts << '-a' if opts[:a] || opts[:annotate]
718
+ arr_opts << '-s' if opts[:s] || opts[:sign]
719
+ arr_opts << '-d' if opts[:d] || opts[:delete]
720
+ arr_opts << name
721
+ arr_opts << target if target
722
+ arr_opts << "-m #{opts[:m] || opts[:message]}" if opts[:m] || opts[:message]
723
+
724
+ command('tag', arr_opts)
725
+ end
726
+
727
+
728
+ def fetch(remote, opts)
729
+ arr_opts = [remote]
730
+ arr_opts << '--tags' if opts[:t] || opts[:tags]
731
+ arr_opts << '--prune' if opts[:p] || opts[:prune]
732
+
733
+ command('fetch', arr_opts)
734
+ end
735
+
736
+ def push(remote, branch = 'master', opts = {})
737
+ # Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature.
738
+ opts = {:tags => opts} if [true, false].include?(opts)
739
+
740
+ arr_opts = []
741
+ arr_opts << '--force' if opts[:force] || opts[:f]
742
+ arr_opts << remote
743
+
744
+ command('push', arr_opts + [branch])
745
+ command('push', ['--tags'] + arr_opts) if opts[:tags]
746
+ end
747
+
748
+ def pull(remote='origin', branch='master')
749
+ command('pull', [remote, branch])
750
+ end
751
+
752
+ def tag_sha(tag_name)
753
+ head = File.join(@git_dir, 'refs', 'tags', tag_name)
754
+ return File.read(head).chomp if File.exist?(head)
755
+
756
+ command('show-ref', ['--tags', '-s', tag_name])
757
+ end
758
+
759
+ def repack
760
+ command('repack', ['-a', '-d'])
761
+ end
762
+
763
+ def gc
764
+ command('gc', ['--prune', '--aggressive', '--auto'])
765
+ end
766
+
767
+ # reads a tree into the current index file
768
+ def read_tree(treeish, opts = {})
769
+ arr_opts = []
770
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
771
+ arr_opts += [treeish]
772
+ command('read-tree', arr_opts)
773
+ end
774
+
775
+ def write_tree
776
+ command('write-tree')
777
+ end
778
+
779
+ def commit_tree(tree, opts = {})
780
+ opts[:message] ||= "commit tree #{tree}"
781
+ t = Tempfile.new('commit-message')
782
+ t.write(opts[:message])
783
+ t.close
784
+
785
+ arr_opts = []
786
+ arr_opts << tree
787
+ arr_opts << '-p' << opts[:parent] if opts[:parent]
788
+ arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents]
789
+ command('commit-tree', arr_opts, true, "< #{escape t.path}")
790
+ end
791
+
792
+ def update_ref(branch, commit)
793
+ command('update-ref', [branch, commit])
794
+ end
795
+
796
+ def checkout_index(opts = {})
797
+ arr_opts = []
798
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
799
+ arr_opts << "--force" if opts[:force]
800
+ arr_opts << "--all" if opts[:all]
801
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
802
+
803
+ command('checkout-index', arr_opts)
804
+ end
805
+
806
+ # creates an archive file
807
+ #
808
+ # options
809
+ # :format (zip, tar)
810
+ # :prefix
811
+ # :remote
812
+ # :path
813
+ def archive(sha, file = nil, opts = {})
814
+ opts[:format] ||= 'zip'
815
+
816
+ if opts[:format] == 'tgz'
817
+ opts[:format] = 'tar'
818
+ opts[:add_gzip] = true
819
+ end
820
+
821
+ if !file
822
+ tempfile = Tempfile.new('archive')
823
+ file = tempfile.path
824
+ # delete it now, before we write to it, so that Ruby doesn't delete it
825
+ # when it finalizes the Tempfile.
826
+ tempfile.close!
827
+ end
828
+
829
+ arr_opts = []
830
+ arr_opts << "--format=#{opts[:format]}" if opts[:format]
831
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
832
+ arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
833
+ arr_opts << sha
834
+ arr_opts << '--' << opts[:path] if opts[:path]
835
+ command('archive', arr_opts, true, (opts[:add_gzip] ? '| gzip' : '') + " > #{escape file}")
836
+ return file
837
+ end
838
+
839
+ # returns the current version of git, as an Array of Fixnums.
840
+ def current_command_version
841
+ output = command('version', [], false)
842
+ version = output[/\d+\.\d+(\.\d+)+/]
843
+ version.split('.').collect {|i| i.to_i}
844
+ end
845
+
846
+ def required_command_version
847
+ [1, 6]
848
+ end
849
+
850
+ def meets_required_version?
851
+ (self.current_command_version <=> self.required_command_version) >= 0
852
+ end
853
+
854
+
855
+ private
856
+
857
+ # Systen ENV variables involved in the git commands.
858
+ #
859
+ # @return [<String>] the names of the EVN variables involved in the git commands
860
+ ENV_VARIABLE_NAMES = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_SSH']
861
+
862
+ def command_lines(cmd, opts = [], chdir = true, redirect = '')
863
+ cmd_op = command(cmd, opts, chdir)
864
+ op = cmd_op.encode("UTF-8", "binary", {
865
+ :invalid => :replace,
866
+ :undef => :replace
867
+ })
868
+ op.split("\n")
869
+ end
870
+
871
+ # Takes the current git's system ENV variables and store them.
872
+ def store_git_system_env_variables
873
+ @git_system_env_variables = {}
874
+ ENV_VARIABLE_NAMES.each do |env_variable_name|
875
+ @git_system_env_variables[env_variable_name] = ENV[env_variable_name]
876
+ end
877
+ end
878
+
879
+ # Takes the previously stored git's ENV variables and set them again on ENV.
880
+ def restore_git_system_env_variables
881
+ ENV_VARIABLE_NAMES.each do |env_variable_name|
882
+ ENV[env_variable_name] = @git_system_env_variables[env_variable_name]
883
+ end
884
+ end
885
+
886
+ # Sets git's ENV variables to the custom values for the current instance.
887
+ def set_custom_git_env_variables
888
+ ENV['GIT_DIR'] = @git_dir
889
+ ENV['GIT_WORK_TREE'] = @git_work_dir
890
+ ENV['GIT_INDEX_FILE'] = @git_index_file
891
+ ENV['GIT_SSH'] = Git::Base.config.git_ssh
892
+ end
893
+
894
+ # Runs a block inside an environment with customized ENV variables.
895
+ # It restores the ENV after execution.
896
+ #
897
+ # @param [Proc] block block to be executed within the customized environment
898
+ def with_custom_env_variables(&block)
899
+ @@semaphore.synchronize do
900
+ store_git_system_env_variables()
901
+ set_custom_git_env_variables()
902
+ return block.call()
903
+ end
904
+ ensure
905
+ restore_git_system_env_variables()
906
+ end
907
+
908
+ def command(cmd, opts = [], chdir = true, redirect = '', &block)
909
+ global_opts = []
910
+ global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
911
+ global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
912
+
913
+ opts = [opts].flatten.map {|s| escape(s) }.join(' ')
914
+
915
+ global_opts = global_opts.flatten.map {|s| escape(s) }.join(' ')
916
+
917
+ git_cmd = "#{Git::Base.config.binary_path} #{global_opts} #{cmd} #{opts} #{redirect} 2>&1"
918
+
919
+ output = nil
920
+
921
+ command_thread = nil;
922
+
923
+ exitstatus = nil
924
+
925
+ with_custom_env_variables do
926
+ command_thread = Thread.new do
927
+ output = run_command(git_cmd, &block)
928
+ exitstatus = $?.exitstatus
929
+ end
930
+ command_thread.join
931
+ end
932
+
933
+ if @logger
934
+ @logger.info(git_cmd)
935
+ @logger.debug(output)
936
+ end
937
+
938
+ if exitstatus > 1 || (exitstatus == 1 && output != '')
939
+ raise Git::GitExecuteError.new(git_cmd + ':' + output.to_s)
940
+ end
941
+
942
+ return output
943
+ end
944
+
945
+ # Takes the diff command line output (as Array) and parse it into a Hash
946
+ #
947
+ # @param [String] diff_command the diff commadn to be used
948
+ # @param [Array] opts the diff options to be used
949
+ # @return [Hash] the diff as Hash
950
+ def diff_as_hash(diff_command, opts=[])
951
+ command_lines(diff_command, opts).inject({}) do |memo, line|
952
+ info, file = line.split("\t")
953
+ mode_src, mode_dest, sha_src, sha_dest, type = info.split
954
+
955
+ memo[file] = {
956
+ :mode_index => mode_dest,
957
+ :mode_repo => mode_src.to_s[1, 7],
958
+ :path => file,
959
+ :sha_repo => sha_src,
960
+ :sha_index => sha_dest,
961
+ :type => type
962
+ }
963
+
964
+ memo
965
+ end
966
+ end
967
+
968
+ # Returns an array holding the common options for the log commands
969
+ #
970
+ # @param [Hash] opts the given options
971
+ # @return [Array] the set of common options that the log command will use
972
+ def log_common_options(opts)
973
+ arr_opts = []
974
+
975
+ arr_opts << "-#{opts[:count]}" if opts[:count]
976
+ arr_opts << "--no-color"
977
+ arr_opts << "--diff-filter=ADM"
978
+ arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
979
+ arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
980
+ arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
981
+ arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
982
+ arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
983
+
984
+ arr_opts
985
+ end
986
+
987
+ # Retrurns an array holding path options for the log commands
988
+ #
989
+ # @param [Hash] opts the given options
990
+ # @return [Array] the set of path options that the log command will use
991
+ def log_path_options(opts)
992
+ arr_opts = []
993
+
994
+ arr_opts << opts[:object] if opts[:object].is_a? String
995
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter]
996
+ arr_opts
997
+ end
998
+
999
+ def run_command(git_cmd, &block)
1000
+ return IO.popen(git_cmd, &block) if block_given?
1001
+
1002
+ `#{git_cmd}`.chomp
1003
+ end
1004
+
1005
+ def escape(s)
1006
+ return "'#{s && s.to_s.gsub('\'','\'"\'"\'')}'" if RUBY_PLATFORM !~ /mingw|mswin/
1007
+
1008
+ # Keeping the old escape format for windows users
1009
+ escaped = s.to_s.gsub('\'', '\'\\\'\'')
1010
+ return %Q{"#{escaped}"}
1011
+ end
1012
+
1013
+ end
1014
+ end