minad-git 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/git/lib.rb ADDED
@@ -0,0 +1,664 @@
1
+ require 'tempfile'
2
+
3
+ module Git
4
+
5
+ class GitExecuteError < StandardError
6
+ end
7
+
8
+ class Lib
9
+
10
+ def initialize(base = nil, logger = nil)
11
+ @git_dir = nil
12
+ @git_index_file = nil
13
+ @git_work_dir = nil
14
+ @path = nil
15
+
16
+ if base.is_a?(Git::Base)
17
+ @git_dir = base.repo.path
18
+ @git_index_file = base.index.path if base.index
19
+ @git_work_dir = base.dir.path if base.dir
20
+ elsif base.is_a?(Hash)
21
+ @git_dir = base[:repository]
22
+ @git_index_file = base[:index]
23
+ @git_work_dir = base[:working_directory]
24
+ end
25
+ @logger = logger
26
+ end
27
+
28
+ def init
29
+ command('init')
30
+ end
31
+
32
+ # tries to clone the given repo
33
+ #
34
+ # returns {:repository} (if bare)
35
+ # {:working_directory} otherwise
36
+ #
37
+ # accepts options:
38
+ # :remote:: name of remote (rather than 'origin')
39
+ # :bare:: no working directory
40
+ # :depth:: the number of commits back to pull
41
+ #
42
+ # TODO - make this work with SSH password or auth_key
43
+ #
44
+ def clone(repository, name, opts = {})
45
+ @path = opts[:path] || '.'
46
+ clone_dir = opts[:path] ? File.join(@path, name) : name
47
+
48
+ arr_opts = []
49
+ arr_opts << "--bare" if opts[:bare]
50
+ arr_opts << "-o" << opts[:remote] if opts[:remote]
51
+ arr_opts << "--depth" << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0
52
+
53
+ arr_opts << '--'
54
+ arr_opts << repository
55
+ arr_opts << clone_dir
56
+
57
+ command('clone', arr_opts)
58
+
59
+ opts[:bare] ? {:repository => clone_dir} : {:working_directory => clone_dir}
60
+ end
61
+
62
+
63
+ ## READ COMMANDS ##
64
+
65
+
66
+ def log_commits(opts = {})
67
+ arr_opts = ['--pretty=oneline']
68
+ arr_opts << "-#{opts[:count]}" if opts[:count]
69
+ arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
70
+ arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
71
+ arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
72
+ arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
73
+ arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
74
+ arr_opts << opts[:object] if opts[:object].is_a? String
75
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
76
+
77
+ command_lines('log', arr_opts, true).map { |l| l.split.first }
78
+ end
79
+
80
+ def full_log_commits(opts = {})
81
+ arr_opts = ['--pretty=raw']
82
+ arr_opts << "-#{opts[:count]}" if opts[:count]
83
+ arr_opts << "--skip=#{opts[:skip]}" if opts[:skip]
84
+ arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
85
+ arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
86
+ arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
87
+ arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
88
+ arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
89
+ arr_opts << opts[:object] if opts[:object].is_a? String
90
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
91
+
92
+ full_log = command_lines('log', arr_opts, true)
93
+ process_commit_data(full_log)
94
+ end
95
+
96
+ def revparse(string)
97
+ return string if string =~ /[A-Fa-f0-9]{40}/ # passing in a sha - just no-op it
98
+ rev = ['head', 'remotes', 'tags'].map do |d|
99
+ File.join(@git_dir, 'refs', d, string)
100
+ end.find do |path|
101
+ File.file?(path)
102
+ end
103
+ return File.read(rev).chomp if rev
104
+ command('rev-parse', string)
105
+ end
106
+
107
+ def namerev(string)
108
+ command('name-rev', string).split[1]
109
+ end
110
+
111
+ def object_type(sha)
112
+ command('cat-file', ['-t', sha])
113
+ end
114
+
115
+ def object_size(sha)
116
+ command('cat-file', ['-s', sha]).to_i
117
+ end
118
+
119
+ # returns useful array of raw commit object data
120
+ def commit_data(sha)
121
+ sha = sha.to_s
122
+ cdata = command_lines('cat-file', ['commit', sha])
123
+ process_commit_data(cdata, sha, 0)
124
+ end
125
+
126
+ def process_commit_data(data, sha = nil, indent = 4)
127
+ in_message = false
128
+
129
+ if sha
130
+ hsh = {'sha' => sha, 'message' => '', 'parent' => []}
131
+ else
132
+ hsh_array = []
133
+ end
134
+
135
+ data.each do |line|
136
+ line = line.chomp
137
+ if line == ''
138
+ in_message = !in_message
139
+ elsif in_message
140
+ hsh['message'] << line[indent..-1] << "\n"
141
+ else
142
+ data = line.split
143
+ key = data.shift
144
+ value = data.join(' ')
145
+ if key == 'commit'
146
+ sha = value
147
+ hsh_array << hsh if hsh
148
+ hsh = {'sha' => sha, 'message' => '', 'parent' => []}
149
+ end
150
+ if key == 'parent'
151
+ hsh[key] << value
152
+ else
153
+ hsh[key] = value
154
+ end
155
+ end
156
+ end
157
+
158
+ if hsh_array
159
+ hsh_array << hsh if hsh
160
+ hsh_array
161
+ else
162
+ hsh
163
+ end
164
+ end
165
+
166
+ def object_contents(sha, &block)
167
+ command('cat-file', ['-p', sha], &block)
168
+ end
169
+
170
+ def ls_tree(sha)
171
+ data = {'blob' => {}, 'tree' => {}}
172
+
173
+ command_lines('ls-tree', sha).each do |line|
174
+ (info, filenm) = line.split("\t")
175
+ (mode, type, sha) = info.split
176
+ data[type][filenm] = {:mode => mode, :sha => sha}
177
+ end
178
+
179
+ data
180
+ end
181
+
182
+ def mv(file1, file2)
183
+ command_lines('mv', ['--', file1, file2])
184
+ end
185
+
186
+ def full_tree(sha)
187
+ command_lines('ls-tree', ['-r', sha])
188
+ end
189
+
190
+ def tree_depth(sha)
191
+ full_tree(sha).size
192
+ end
193
+
194
+ def change_head_branch(branch_name)
195
+ command('symbolic-ref', ['HEAD', "refs/heads/#{branch_name}"])
196
+ end
197
+
198
+ def branches_all
199
+ arr = []
200
+ command_lines('branch', '-a').each do |b|
201
+ current = (b[0, 2] == '* ')
202
+ arr << [b.gsub('* ', '').strip, current]
203
+ end
204
+ arr
205
+ end
206
+
207
+ def list_files(ref_dir)
208
+ dir = File.join(@git_dir, 'refs', ref_dir)
209
+ files = []
210
+ Dir.chdir(dir) { files = Dir.glob('**/*').select { |f| File.file?(f) } } rescue nil
211
+ files
212
+ end
213
+
214
+ def branch_current
215
+ branches_all.select { |b| b[1] }.first[0] rescue nil
216
+ end
217
+
218
+
219
+ # returns hash
220
+ # [tree-ish] = [[line_no, match], [line_no, match2]]
221
+ # [tree-ish] = [[line_no, match], [line_no, match2]]
222
+ def grep(string, opts = {})
223
+ opts[:object] ||= 'HEAD'
224
+
225
+ grep_opts = ['-n']
226
+ grep_opts << '-i' if opts[:ignore_case]
227
+ grep_opts << '-v' if opts[:invert_match]
228
+ grep_opts << '-e'
229
+ grep_opts << string
230
+ grep_opts << opts[:object] if opts[:object].is_a?(String)
231
+ grep_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
232
+
233
+ hsh = {}
234
+ command_lines('grep', grep_opts).each do |line|
235
+ if m = /(.*)\:(\d+)\:(.*)/.match(line)
236
+ hsh[m[1]] ||= []
237
+ hsh[m[1]] << [m[2].to_i, m[3]]
238
+ end
239
+ end
240
+ hsh
241
+ end
242
+
243
+ def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
244
+ diff_opts = ['-p']
245
+ diff_opts << obj1
246
+ diff_opts << obj2 if obj2.is_a?(String)
247
+ diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
248
+
249
+ command('diff', diff_opts)
250
+ end
251
+
252
+ def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
253
+ diff_opts = ['--numstat']
254
+ diff_opts << obj1
255
+ diff_opts << obj2 if obj2.is_a?(String)
256
+ diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
257
+
258
+ hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}}
259
+
260
+ command_lines('diff', diff_opts).each do |file|
261
+ (insertions, deletions, filename) = file.split("\t")
262
+ hsh[:total][:insertions] += insertions.to_i
263
+ hsh[:total][:deletions] += deletions.to_i
264
+ hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions])
265
+ hsh[:total][:files] += 1
266
+ hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i}
267
+ end
268
+
269
+ hsh
270
+ end
271
+
272
+ # compares the index and the working directory
273
+ def diff_files
274
+ hsh = {}
275
+ command_lines('diff-files').each do |line|
276
+ (info, file) = line.split("\t")
277
+ (mode_src, mode_dest, sha_src, sha_dest, type) = info.split
278
+ hsh[file] = {:path => file, :mode_file => mode_src.to_s[1, 7], :mode_index => mode_dest,
279
+ :sha_file => sha_src, :sha_index => sha_dest, :type => type}
280
+ end
281
+ hsh
282
+ end
283
+
284
+ # compares the index and the repository
285
+ def diff_index(treeish)
286
+ hsh = {}
287
+ command_lines('diff-index', treeish).each do |line|
288
+ (info, file) = line.split("\t")
289
+ (mode_src, mode_dest, sha_src, sha_dest, type) = info.split
290
+ hsh[file] = {:path => file, :mode_repo => mode_src.to_s[1, 7], :mode_index => mode_dest,
291
+ :sha_repo => sha_src, :sha_index => sha_dest, :type => type}
292
+ end
293
+ hsh
294
+ end
295
+
296
+ def ls_files
297
+ hsh = {}
298
+ command_lines('ls-files', '--stage').each do |line|
299
+ (info, file) = line.split("\t")
300
+ (mode, sha, stage) = info.split
301
+ hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
302
+ end
303
+ hsh
304
+ end
305
+
306
+
307
+ def config_remote(name)
308
+ hsh = {}
309
+ config_list.each do |key, value|
310
+ if /remote.#{name}/.match(key)
311
+ hsh[key.gsub("remote.#{name}.", '')] = value
312
+ end
313
+ end
314
+ hsh
315
+ end
316
+
317
+ def config_get(name)
318
+ config_list[name]
319
+ #command('config', ['--get', name])
320
+ end
321
+
322
+ def config_list
323
+ config = {}
324
+ config.merge!(parse_config('~/.gitconfig'))
325
+ config.merge!(parse_config(File.join(@git_dir, 'config')))
326
+ #hsh = {}
327
+ #command_lines('config', ['--list']).each do |line|
328
+ # (key, value) = line.split('=')
329
+ # hsh[key] = value
330
+ #end
331
+ #hsh
332
+ end
333
+
334
+ def parse_config(file)
335
+ hsh = {}
336
+ file = File.expand_path(file)
337
+ if File.file?(file)
338
+ current_section = nil
339
+ File.readlines(file).each do |line|
340
+ if m = /\[(\w+)\]/.match(line)
341
+ current_section = m[1]
342
+ elsif m = /\[(\w+?) "(.*?)"\]/.match(line)
343
+ current_section = "#{m[1]}.#{m[2]}"
344
+ elsif m = /(\w+?) = (.*)/.match(line)
345
+ key = "#{current_section}.#{m[1]}"
346
+ hsh[key] = m[2]
347
+ end
348
+ end
349
+ end
350
+ hsh
351
+ end
352
+
353
+ ## WRITE COMMANDS ##
354
+
355
+ def config_set(name, value)
356
+ command('config', [name, value])
357
+ end
358
+
359
+ def add(path = '.')
360
+ arr_opts = ['--']
361
+ if path.is_a?(Array)
362
+ arr_opts += path
363
+ else
364
+ arr_opts << path
365
+ end
366
+ command('add', arr_opts)
367
+ end
368
+
369
+ def remove(path = '.', opts = {})
370
+ arr_opts = ['-f'] # overrides the up-to-date check by default
371
+ arr_opts << ['-r'] if opts[:recursive]
372
+ arr_opts << '--'
373
+ if path.is_a?(Array)
374
+ arr_opts += path
375
+ else
376
+ arr_opts << path
377
+ end
378
+
379
+ command('rm', arr_opts)
380
+ end
381
+
382
+ def commit(message, opts = {})
383
+ arr_opts = ['-m', message]
384
+ arr_opts << '-a' if opts[:add_all]
385
+ arr_opts << '--allow-empty' if opts[:allow_empty]
386
+ arr_opts << "--author" << opts[:author] if opts[:author]
387
+ command('commit', arr_opts)
388
+ end
389
+
390
+ def reset(commit, opts = {})
391
+ arr_opts = []
392
+ arr_opts << '--hard' if opts[:hard]
393
+ arr_opts << commit if commit
394
+ command('reset', arr_opts)
395
+ end
396
+
397
+ def revert(commit)
398
+ command('revert', ['--no-edit', commit])
399
+ end
400
+
401
+ def apply(patch_file)
402
+ arr_opts = []
403
+ arr_opts << '--' << patch_file if patch_file
404
+ command('apply', arr_opts)
405
+ end
406
+
407
+ def apply_mail(patch_file)
408
+ arr_opts = []
409
+ arr_opts << '--' << patch_file if patch_file
410
+ command('am', arr_opts)
411
+ end
412
+
413
+ def stashes_all
414
+ arr = []
415
+ filename = File.join(@git_dir, 'logs/refs/stash')
416
+ if File.exist?(filename)
417
+ File.open(filename).each_with_index { |line, i|
418
+ m = line.match(/:(.*)$/)
419
+ arr << [i, m[1].strip]
420
+ }
421
+ end
422
+ arr
423
+ end
424
+
425
+ def stash_save(message)
426
+ output = command('stash save', ['--', message])
427
+ output =~ /HEAD is now at/
428
+ end
429
+
430
+ def stash_apply(id = nil)
431
+ if id
432
+ command('stash apply', [id])
433
+ else
434
+ command('stash apply')
435
+ end
436
+ end
437
+
438
+ def stash_clear
439
+ command('stash clear')
440
+ end
441
+
442
+ def stash_list
443
+ command('stash list')
444
+ end
445
+
446
+ def branch_new(branch)
447
+ command('branch', branch)
448
+ end
449
+
450
+ def branch_delete(branch)
451
+ command('branch', ['-D', branch])
452
+ end
453
+
454
+ def checkout(branch, opts = {})
455
+ arr_opts = []
456
+ arr_opts << '-f' if opts[:force]
457
+ arr_opts << '-b' << opts[:new_branch] if opts[:new_branch]
458
+ arr_opts << branch
459
+
460
+ command('checkout', arr_opts)
461
+ end
462
+
463
+ def checkout_file(version, file)
464
+ arr_opts = []
465
+ arr_opts << version
466
+ arr_opts << file
467
+ command('checkout', arr_opts)
468
+ end
469
+
470
+ def merge(branch, message = nil)
471
+ arr_opts = []
472
+ arr_opts << '-m' << message if message
473
+ arr_opts += [branch]
474
+ command('merge', arr_opts)
475
+ end
476
+
477
+ def unmerged
478
+ unmerged = []
479
+ command_lines('diff', ["--cached"]).each do |line|
480
+ unmerged << $1 if line =~ /^\* Unmerged path (.*)/
481
+ end
482
+ unmerged
483
+ end
484
+
485
+ def conflicts # :yields: file, your, their
486
+ self.unmerged.each do |f|
487
+ your = Tempfile.new("YOUR-#{File.basename(f)}").path
488
+ command('show', ":2:#{f}", true, "> #{escape your}")
489
+
490
+ their = Tempfile.new("THEIR-#{File.basename(f)}").path
491
+ command('show', ":3:#{f}", true, "> #{escape their}")
492
+ yield(f, your, their)
493
+ end
494
+ end
495
+
496
+ def remote_add(name, url, opts = {})
497
+ arr_opts = ['add']
498
+ arr_opts << '-f' if opts[:with_fetch]
499
+ arr_opts << '--'
500
+ arr_opts << name
501
+ arr_opts << url
502
+
503
+ command('remote', arr_opts)
504
+ end
505
+
506
+ # this is documented as such, but seems broken for some reason
507
+ # i'll try to get around it some other way later
508
+ def remote_remove(name)
509
+ command('remote', ['rm', '--', name])
510
+ end
511
+
512
+ def remotes
513
+ command_lines('remote')
514
+ end
515
+
516
+ def tags
517
+ command_lines('tag')
518
+ end
519
+
520
+ def tag(tag)
521
+ command('tag', tag)
522
+ end
523
+
524
+
525
+ def fetch(remote)
526
+ command('fetch', remote)
527
+ end
528
+
529
+ def push(remote, branch = 'master', tags = false)
530
+ command('push', [remote, branch])
531
+ command('push', ['--tags', remote]) if tags
532
+ end
533
+
534
+ def tag_sha(tag_name)
535
+ head = File.join(@git_dir, 'refs', 'tags', tag_name)
536
+ return File.read(head).chomp if File.exists?(head)
537
+
538
+ command('show-ref', ['--tags', '-s', tag_name])
539
+ end
540
+
541
+ def repack
542
+ command('repack', ['-a', '-d'])
543
+ end
544
+
545
+ def gc
546
+ command('gc', ['--prune', '--aggressive', '--auto'])
547
+ end
548
+
549
+ # reads a tree into the current index file
550
+ def read_tree(treeish, opts = {})
551
+ arr_opts = []
552
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
553
+ arr_opts += [treeish]
554
+ command('read-tree', arr_opts)
555
+ end
556
+
557
+ def write_tree
558
+ command('write-tree')
559
+ end
560
+
561
+ def commit_tree(tree, opts = {})
562
+ opts[:message] ||= "commit tree #{tree}"
563
+ t = Tempfile.new('commit-message')
564
+ t.write(opts[:message])
565
+ t.close
566
+
567
+ arr_opts = []
568
+ arr_opts << tree
569
+ arr_opts << '-p' << opts[:parent] if opts[:parent]
570
+ arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents]
571
+ command('commit-tree', arr_opts, true, "< #{escape t.path}")
572
+ end
573
+
574
+ def update_ref(branch, commit)
575
+ command('update-ref', [branch, commit])
576
+ end
577
+
578
+ def checkout_index(opts = {})
579
+ arr_opts = []
580
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
581
+ arr_opts << "--force" if opts[:force]
582
+ arr_opts << "--all" if opts[:all]
583
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
584
+
585
+ command('checkout-index', arr_opts)
586
+ end
587
+
588
+ # creates an archive file
589
+ #
590
+ # options
591
+ # :format (zip, tar)
592
+ # :prefix
593
+ # :remote
594
+ # :path
595
+ def archive(sha, file = nil, opts = {})
596
+ opts[:format] ||= 'zip'
597
+
598
+ if opts[:format] == 'tgz'
599
+ opts[:format] = 'tar'
600
+ opts[:add_gzip] = true
601
+ end
602
+
603
+ file ||= Tempfile.new('archive').path
604
+
605
+ arr_opts = []
606
+ arr_opts << "--format=#{opts[:format]}" if opts[:format]
607
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
608
+ arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
609
+ arr_opts << sha
610
+ arr_opts << '--' << opts[:path] if opts[:path]
611
+ command('archive', arr_opts, true, (opts[:add_gzip] ? '| gzip' : '') + " > #{escape file}")
612
+ return file
613
+ end
614
+
615
+ private
616
+
617
+ def command_lines(cmd, opts = [], chdir = true, redirect = '')
618
+ command(cmd, opts, chdir).split("\n")
619
+ end
620
+
621
+ def command(cmd, opts = [], chdir = true, redirect = '', &block)
622
+ ENV['GIT_DIR'] = @git_dir
623
+ ENV['GIT_INDEX_FILE'] = @git_index_file
624
+ ENV['GIT_WORK_TREE'] = @git_work_dir
625
+ path = @git_work_dir || @git_dir || @path
626
+
627
+ opts = [opts].flatten.map {|s| escape(s) }.join(' ')
628
+ git_cmd = "git #{cmd} #{opts} #{redirect} 2>&1"
629
+
630
+ out = nil
631
+ if chdir && (Dir.getwd != path)
632
+ Dir.chdir(path) { out = run_command(git_cmd, &block) }
633
+ else
634
+ out = run_command(git_cmd, &block)
635
+ end
636
+
637
+ if @logger
638
+ @logger.info(git_cmd)
639
+ @logger.debug(out)
640
+ end
641
+
642
+ if $?.exitstatus > 0
643
+ if $?.exitstatus == 1 && out == ''
644
+ return ''
645
+ end
646
+ raise Git::GitExecuteError.new(git_cmd + ':' + out.to_s)
647
+ end
648
+ out
649
+ end
650
+
651
+ def run_command(git_cmd, &block)
652
+ if block_given?
653
+ IO.popen(git_cmd, &block)
654
+ else
655
+ `#{git_cmd}`.chomp
656
+ end
657
+ end
658
+
659
+ def escape(s)
660
+ "'" + s.to_s.gsub('\'', '\'\\\'\'') + "'"
661
+ end
662
+
663
+ end
664
+ end