peterwald-git 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/git/branch.rb ADDED
@@ -0,0 +1,104 @@
1
+ module Git
2
+ class Branch < Path
3
+
4
+ attr_accessor :full, :remote, :name
5
+
6
+ def initialize(base, name)
7
+ @remote = nil
8
+ @full = name
9
+ @base = base
10
+ @gcommit = nil
11
+ @stashes = nil
12
+
13
+ parts = name.split('/')
14
+ if parts[1]
15
+ @remote = Git::Remote.new(@base, parts[0])
16
+ @name = parts[1]
17
+ else
18
+ @name = parts[0]
19
+ end
20
+ end
21
+
22
+ def gcommit
23
+ @gcommit ||= @base.gcommit(@full)
24
+ @gcommit
25
+ end
26
+
27
+ def stashes
28
+ @stashes ||= Git::Stashes.new(@base)
29
+ end
30
+
31
+ def checkout
32
+ check_if_create
33
+ @base.checkout(@full)
34
+ end
35
+
36
+ def archive(file, opts = {})
37
+ @base.lib.archive(@full, file, opts)
38
+ end
39
+
40
+ # g.branch('new_branch').in_branch do
41
+ # # create new file
42
+ # # do other stuff
43
+ # return true # auto commits and switches back
44
+ # end
45
+ def in_branch (message = 'in branch work')
46
+ old_current = @base.lib.branch_current
47
+ checkout
48
+ if yield
49
+ @base.commit_all(message)
50
+ else
51
+ @base.reset_hard
52
+ end
53
+ @base.checkout(old_current)
54
+ end
55
+
56
+ def create
57
+ check_if_create
58
+ end
59
+
60
+ def delete
61
+ @base.lib.branch_delete(@name)
62
+ end
63
+
64
+ def current
65
+ determine_current
66
+ end
67
+
68
+ def merge(branch = nil, message = nil)
69
+ if branch
70
+ in_branch do
71
+ @base.merge(branch, message)
72
+ false
73
+ end
74
+ # merge a branch into this one
75
+ else
76
+ # merge this branch into the current one
77
+ @base.merge(@name)
78
+ end
79
+ end
80
+
81
+ def update_ref(commit)
82
+ @base.lib.update_ref(@full, commit)
83
+ end
84
+
85
+ def to_a
86
+ [@full]
87
+ end
88
+
89
+ def to_s
90
+ @full
91
+ end
92
+
93
+ private
94
+
95
+ def check_if_create
96
+ @base.lib.branch_new(@name) rescue nil
97
+ end
98
+
99
+ def determine_current
100
+ @base.lib.branch_current == @name
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,48 @@
1
+ module Git
2
+
3
+ # object that holds all the available branches
4
+ class Branches
5
+ include Enumerable
6
+
7
+ def initialize(base)
8
+ @branches = {}
9
+
10
+ @base = base
11
+
12
+ @base.lib.branches_all.each do |b|
13
+ @branches[b[0]] = Git::Branch.new(@base, b[0])
14
+ end
15
+ end
16
+
17
+ def local
18
+ self.select { |b| !b.remote }
19
+ end
20
+
21
+ def remote
22
+ self.select { |b| b.remote }
23
+ end
24
+
25
+ # array like methods
26
+
27
+ def size
28
+ @branches.size
29
+ end
30
+
31
+ def each(&block)
32
+ @branches.values.each(&block)
33
+ end
34
+
35
+ def [](symbol)
36
+ @branches[symbol.to_s]
37
+ end
38
+
39
+ def to_s
40
+ out = ''
41
+ @branches.each do |k, b|
42
+ out << (b.current ? '* ' : ' ') << b.to_s << "\n"
43
+ end
44
+ out
45
+ end
46
+
47
+ end
48
+ end
data/lib/git/diff.rb ADDED
@@ -0,0 +1,146 @@
1
+ module Git
2
+
3
+ # object that holds the last X commits on given branch
4
+ class Diff
5
+ include Enumerable
6
+
7
+ def initialize(base, from = nil, to = nil)
8
+ @base = base
9
+ @from = from.to_s
10
+ @to = to.to_s
11
+
12
+ @path = nil
13
+ @full_diff = nil
14
+ @full_diff_files = nil
15
+ @stats = nil
16
+ end
17
+ attr_reader :from, :to
18
+
19
+ def path(path)
20
+ @path = path
21
+ return self
22
+ end
23
+
24
+ def size
25
+ cache_stats
26
+ @stats[:total][:files]
27
+ end
28
+
29
+ def lines
30
+ cache_stats
31
+ @stats[:total][:lines]
32
+ end
33
+
34
+ def deletions
35
+ cache_stats
36
+ @stats[:total][:deletions]
37
+ end
38
+
39
+ def insertions
40
+ cache_stats
41
+ @stats[:total][:insertions]
42
+ end
43
+
44
+ def stats
45
+ cache_stats
46
+ @stats
47
+ end
48
+
49
+ # if file is provided and is writable, it will write the patch into the file
50
+ def patch(file = nil)
51
+ cache_full
52
+ @full_diff
53
+ end
54
+ alias_method :to_s, :patch
55
+
56
+ # enumerable methods
57
+
58
+ def [](key)
59
+ process_full
60
+ @full_diff_files.assoc(key)[1]
61
+ end
62
+
63
+ def each(&block) # :yields: each Git::DiffFile in turn
64
+ process_full
65
+ @full_diff_files.map { |file| file[1] }.each(&block)
66
+ end
67
+
68
+ class DiffFile
69
+ attr_accessor :patch, :path, :mode, :src, :dst, :type
70
+ @base = nil
71
+
72
+ def initialize(base, hash)
73
+ @base = base
74
+ @patch = hash[:patch]
75
+ @path = hash[:path]
76
+ @mode = hash[:mode]
77
+ @src = hash[:src]
78
+ @dst = hash[:dst]
79
+ @type = hash[:type]
80
+ @binary = hash[:binary]
81
+ end
82
+
83
+ def binary?
84
+ !!@binary
85
+ end
86
+
87
+ def blob(type = :dst)
88
+ if type == :src
89
+ @base.object(@src) if @src != '0000000'
90
+ else
91
+ @base.object(@dst) if @dst != '0000000'
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def cache_full
99
+ unless @full_diff
100
+ @full_diff = @base.lib.diff_full(@from, @to, {:path_limiter => @path})
101
+ end
102
+ end
103
+
104
+ def process_full
105
+ unless @full_diff_files
106
+ cache_full
107
+ @full_diff_files = process_full_diff
108
+ end
109
+ end
110
+
111
+ def cache_stats
112
+ unless @stats
113
+ @stats = @base.lib.diff_stats(@from, @to, {:path_limiter => @path})
114
+ end
115
+ end
116
+
117
+ # break up @diff_full
118
+ def process_full_diff
119
+ final = {}
120
+ current_file = nil
121
+ @full_diff.split("\n").each do |line|
122
+ if m = /diff --git a\/(.*?) b\/(.*?)/.match(line)
123
+ current_file = m[1]
124
+ final[current_file] = {:patch => line, :path => current_file,
125
+ :mode => '', :src => '', :dst => '', :type => 'modified'}
126
+ else
127
+ if m = /index (.......)\.\.(.......)( ......)*/.match(line)
128
+ final[current_file][:src] = m[1]
129
+ final[current_file][:dst] = m[2]
130
+ final[current_file][:mode] = m[3].strip if m[3]
131
+ end
132
+ if m = /(.*?) file mode (......)/.match(line)
133
+ final[current_file][:type] = m[1]
134
+ final[current_file][:mode] = m[2]
135
+ end
136
+ if m = /^Binary files /.match(line)
137
+ final[current_file][:binary] = true
138
+ end
139
+ final[current_file][:patch] << "\n" + line
140
+ end
141
+ end
142
+ final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
143
+ end
144
+
145
+ end
146
+ end
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,660 @@
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 apply(patch_file)
398
+ arr_opts = []
399
+ arr_opts << '--' << patch_file if patch_file
400
+ command('apply', arr_opts)
401
+ end
402
+
403
+ def apply_mail(patch_file)
404
+ arr_opts = []
405
+ arr_opts << '--' << patch_file if patch_file
406
+ command('am', arr_opts)
407
+ end
408
+
409
+ def stashes_all
410
+ arr = []
411
+ filename = File.join(@git_dir, 'logs/refs/stash')
412
+ if File.exist?(filename)
413
+ File.open(filename).each_with_index { |line, i|
414
+ m = line.match(/:(.*)$/)
415
+ arr << [i, m[1].strip]
416
+ }
417
+ end
418
+ arr
419
+ end
420
+
421
+ def stash_save(message)
422
+ output = command('stash save', ['--', message])
423
+ output =~ /HEAD is now at/
424
+ end
425
+
426
+ def stash_apply(id = nil)
427
+ if id
428
+ command('stash apply', [id])
429
+ else
430
+ command('stash apply')
431
+ end
432
+ end
433
+
434
+ def stash_clear
435
+ command('stash clear')
436
+ end
437
+
438
+ def stash_list
439
+ command('stash list')
440
+ end
441
+
442
+ def branch_new(branch)
443
+ command('branch', branch)
444
+ end
445
+
446
+ def branch_delete(branch)
447
+ command('branch', ['-D', branch])
448
+ end
449
+
450
+ def checkout(branch, opts = {})
451
+ arr_opts = []
452
+ arr_opts << '-f' if opts[:force]
453
+ arr_opts << '-b' << opts[:new_branch] if opts[:new_branch]
454
+ arr_opts << branch
455
+
456
+ command('checkout', arr_opts)
457
+ end
458
+
459
+ def checkout_file(version, file)
460
+ arr_opts = []
461
+ arr_opts << version
462
+ arr_opts << file
463
+ command('checkout', arr_opts)
464
+ end
465
+
466
+ def merge(branch, message = nil)
467
+ arr_opts = []
468
+ arr_opts << '-m' << message if message
469
+ arr_opts += [branch]
470
+ command('merge', arr_opts)
471
+ end
472
+
473
+ def unmerged
474
+ unmerged = []
475
+ command_lines('diff', ["--cached"]).each do |line|
476
+ unmerged << $1 if line =~ /^\* Unmerged path (.*)/
477
+ end
478
+ unmerged
479
+ end
480
+
481
+ def conflicts # :yields: file, your, their
482
+ self.unmerged.each do |f|
483
+ your = Tempfile.new("YOUR-#{File.basename(f)}").path
484
+ command('show', ":2:#{f}", true, "> #{escape your}")
485
+
486
+ their = Tempfile.new("THEIR-#{File.basename(f)}").path
487
+ command('show', ":3:#{f}", true, "> #{escape their}")
488
+ yield(f, your, their)
489
+ end
490
+ end
491
+
492
+ def remote_add(name, url, opts = {})
493
+ arr_opts = ['add']
494
+ arr_opts << '-f' if opts[:with_fetch]
495
+ arr_opts << '--'
496
+ arr_opts << name
497
+ arr_opts << url
498
+
499
+ command('remote', arr_opts)
500
+ end
501
+
502
+ # this is documented as such, but seems broken for some reason
503
+ # i'll try to get around it some other way later
504
+ def remote_remove(name)
505
+ command('remote', ['rm', '--', name])
506
+ end
507
+
508
+ def remotes
509
+ command_lines('remote')
510
+ end
511
+
512
+ def tags
513
+ command_lines('tag')
514
+ end
515
+
516
+ def tag(tag)
517
+ command('tag', tag)
518
+ end
519
+
520
+
521
+ def fetch(remote)
522
+ command('fetch', remote)
523
+ end
524
+
525
+ def push(remote, branch = 'master', tags = false)
526
+ command('push', [remote, branch])
527
+ command('push', ['--tags', remote]) if tags
528
+ end
529
+
530
+ def tag_sha(tag_name)
531
+ head = File.join(@git_dir, 'refs', 'tags', tag_name)
532
+ return File.read(head).chomp if File.exists?(head)
533
+
534
+ command('show-ref', ['--tags', '-s', tag_name])
535
+ end
536
+
537
+ def repack
538
+ command('repack', ['-a', '-d'])
539
+ end
540
+
541
+ def gc
542
+ command('gc', ['--prune', '--aggressive', '--auto'])
543
+ end
544
+
545
+ # reads a tree into the current index file
546
+ def read_tree(treeish, opts = {})
547
+ arr_opts = []
548
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
549
+ arr_opts += [treeish]
550
+ command('read-tree', arr_opts)
551
+ end
552
+
553
+ def write_tree
554
+ command('write-tree')
555
+ end
556
+
557
+ def commit_tree(tree, opts = {})
558
+ opts[:message] ||= "commit tree #{tree}"
559
+ t = Tempfile.new('commit-message')
560
+ t.write(opts[:message])
561
+ t.close
562
+
563
+ arr_opts = []
564
+ arr_opts << tree
565
+ arr_opts << '-p' << opts[:parent] if opts[:parent]
566
+ arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents]
567
+ command('commit-tree', arr_opts, true, "< #{escape t.path}")
568
+ end
569
+
570
+ def update_ref(branch, commit)
571
+ command('update-ref', [branch, commit])
572
+ end
573
+
574
+ def checkout_index(opts = {})
575
+ arr_opts = []
576
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
577
+ arr_opts << "--force" if opts[:force]
578
+ arr_opts << "--all" if opts[:all]
579
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
580
+
581
+ command('checkout-index', arr_opts)
582
+ end
583
+
584
+ # creates an archive file
585
+ #
586
+ # options
587
+ # :format (zip, tar)
588
+ # :prefix
589
+ # :remote
590
+ # :path
591
+ def archive(sha, file = nil, opts = {})
592
+ opts[:format] ||= 'zip'
593
+
594
+ if opts[:format] == 'tgz'
595
+ opts[:format] = 'tar'
596
+ opts[:add_gzip] = true
597
+ end
598
+
599
+ file ||= Tempfile.new('archive').path
600
+
601
+ arr_opts = []
602
+ arr_opts << "--format=#{opts[:format]}" if opts[:format]
603
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
604
+ arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
605
+ arr_opts << sha
606
+ arr_opts << '--' << opts[:path] if opts[:path]
607
+ command('archive', arr_opts, true, (opts[:add_gzip] ? '| gzip' : '') + " > #{escape file}")
608
+ return file
609
+ end
610
+
611
+ private
612
+
613
+ def command_lines(cmd, opts = [], chdir = true, redirect = '')
614
+ command(cmd, opts, chdir).split("\n")
615
+ end
616
+
617
+ def command(cmd, opts = [], chdir = true, redirect = '', &block)
618
+ ENV['GIT_DIR'] = @git_dir
619
+ ENV['GIT_INDEX_FILE'] = @git_index_file
620
+ ENV['GIT_WORK_TREE'] = @git_work_dir
621
+ path = @git_work_dir || @git_dir || @path
622
+
623
+ opts = [opts].flatten.map {|s| escape(s) }.join(' ')
624
+ git_cmd = "git #{cmd} #{opts} #{redirect} 2>&1"
625
+
626
+ out = nil
627
+ if chdir && (Dir.getwd != path)
628
+ Dir.chdir(path) { out = run_command(git_cmd, &block) }
629
+ else
630
+ out = run_command(git_cmd, &block)
631
+ end
632
+
633
+ if @logger
634
+ @logger.info(git_cmd)
635
+ @logger.debug(out)
636
+ end
637
+
638
+ if $?.exitstatus > 0
639
+ if $?.exitstatus == 1 && out == ''
640
+ return ''
641
+ end
642
+ raise Git::GitExecuteError.new(git_cmd + ':' + out.to_s)
643
+ end
644
+ out
645
+ end
646
+
647
+ def run_command(git_cmd, &block)
648
+ if block_given?
649
+ IO.popen(git_cmd, &block)
650
+ else
651
+ `#{git_cmd}`.chomp
652
+ end
653
+ end
654
+
655
+ def escape(s)
656
+ "'" + s.to_s.gsub('\'', '\'\\\'\'') + "'"
657
+ end
658
+
659
+ end
660
+ end