arpm 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ require 'git/path'
2
+
3
+ module Git
4
+
5
+ class Branch < Path
6
+
7
+ attr_accessor :full, :remote, :name
8
+
9
+ def initialize(base, name)
10
+ @full = name
11
+ @base = base
12
+ @gcommit = nil
13
+ @stashes = nil
14
+ @remote, @name = parse_name(name)
15
+ end
16
+
17
+ def gcommit
18
+ @gcommit ||= @base.gcommit(@full)
19
+ @gcommit
20
+ end
21
+
22
+ def stashes
23
+ @stashes ||= Git::Stashes.new(@base)
24
+ end
25
+
26
+ def checkout
27
+ check_if_create
28
+ @base.checkout(@full)
29
+ end
30
+
31
+ def archive(file, opts = {})
32
+ @base.lib.archive(@full, file, opts)
33
+ end
34
+
35
+ # g.branch('new_branch').in_branch do
36
+ # # create new file
37
+ # # do other stuff
38
+ # return true # auto commits and switches back
39
+ # end
40
+ def in_branch (message = 'in branch work')
41
+ old_current = @base.lib.branch_current
42
+ checkout
43
+ if yield
44
+ @base.commit_all(message)
45
+ else
46
+ @base.reset_hard
47
+ end
48
+ @base.checkout(old_current)
49
+ end
50
+
51
+ def create
52
+ check_if_create
53
+ end
54
+
55
+ def delete
56
+ @base.lib.branch_delete(@name)
57
+ end
58
+
59
+ def current
60
+ determine_current
61
+ end
62
+
63
+ def merge(branch = nil, message = nil)
64
+ if branch
65
+ in_branch do
66
+ @base.merge(branch, message)
67
+ false
68
+ end
69
+ # merge a branch into this one
70
+ else
71
+ # merge this branch into the current one
72
+ @base.merge(@name)
73
+ end
74
+ end
75
+
76
+ def update_ref(commit)
77
+ @base.lib.update_ref(@full, commit)
78
+ end
79
+
80
+ def to_a
81
+ [@full]
82
+ end
83
+
84
+ def to_s
85
+ @full
86
+ end
87
+
88
+ private
89
+
90
+ def check_if_create
91
+ @base.lib.branch_new(@name) rescue nil
92
+ end
93
+
94
+ def determine_current
95
+ @base.lib.branch_current == @name
96
+ end
97
+
98
+ # Given a full branch name return an Array containing the remote and branch names.
99
+ #
100
+ # Removes 'remotes' from the beggining of the name (if present).
101
+ # Takes the second part (splittign by '/') as the remote name.
102
+ # Takes the rest as the repo name (can also hold one or more '/').
103
+ #
104
+ # Example:
105
+ # parse_name('master') #=> [nil, 'master']
106
+ # parse_name('origin/master') #=> ['origin', 'master']
107
+ # parse_name('remotes/origin/master') #=> ['origin', 'master']
108
+ # parse_name('origin/master/v2') #=> ['origin', 'master/v2']
109
+ #
110
+ # param [String] name branch full name.
111
+ # return [<Git::Remote,NilClass,String>] an Array containing the remote and branch names.
112
+ def parse_name(name)
113
+ if name.match(/^(?:remotes)?\/([^\/]+)\/(.+)/)
114
+ return [Git::Remote.new(@base, $1), $2]
115
+ end
116
+
117
+ return [nil, name]
118
+ end
119
+
120
+ end
121
+
122
+ end
@@ -0,0 +1,71 @@
1
+ module Git
2
+
3
+ # object that holds all the available branches
4
+ class Branches
5
+
6
+ include Enumerable
7
+
8
+ def initialize(base)
9
+ @branches = {}
10
+
11
+ @base = base
12
+
13
+ @base.lib.branches_all.each do |b|
14
+ @branches[b[0]] = Git::Branch.new(@base, b[0])
15
+ end
16
+ end
17
+
18
+ def local
19
+ self.select { |b| !b.remote }
20
+ end
21
+
22
+ def remote
23
+ self.select { |b| b.remote }
24
+ end
25
+
26
+ # array like methods
27
+
28
+ def size
29
+ @branches.size
30
+ end
31
+
32
+ def each(&block)
33
+ @branches.values.each(&block)
34
+ end
35
+
36
+ # Returns the target branch
37
+ #
38
+ # Example:
39
+ # Given (git branch -a):
40
+ # master
41
+ # remotes/working/master
42
+ #
43
+ # g.branches['master'].full #=> 'master'
44
+ # g.branches['working/master'].full => 'remotes/working/master'
45
+ # g.branches['remotes/working/master'].full => 'remotes/working/master'
46
+ #
47
+ # @param [#to_s] branch_name the target branch name.
48
+ # @return [Git::Branch] the target branch.
49
+ def [](branch_name)
50
+ @branches.values.inject(@branches) do |branches, branch|
51
+ branches[branch.full] ||= branch
52
+
53
+ # This is how Git (version 1.7.9.5) works.
54
+ # Lets you ignore the 'remotes' if its at the beginning of the branch full name (even if is not a real remote branch).
55
+ branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ /^remotes\/.+/
56
+
57
+ branches
58
+ end[branch_name.to_s]
59
+ end
60
+
61
+ def to_s
62
+ out = ''
63
+ @branches.each do |k, b|
64
+ out << (b.current ? '* ' : ' ') << b.to_s << "\n"
65
+ end
66
+ out
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ module Git
2
+ class Index < Git::Path
3
+
4
+ end
5
+ end
@@ -0,0 +1,848 @@
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
+ # creates or reinitializes the repository
29
+ #
30
+ # options:
31
+ # :bare
32
+ # :working_directory
33
+ #
34
+ def init(opts={})
35
+ arr_opts = []
36
+ arr_opts << '--bare' if opts[:bare]
37
+
38
+ command('init', arr_opts, false)
39
+ end
40
+
41
+ # tries to clone the given repo
42
+ #
43
+ # returns {:repository} (if bare)
44
+ # {:working_directory} otherwise
45
+ #
46
+ # accepts options:
47
+ # :remote:: name of remote (rather than 'origin')
48
+ # :bare:: no working directory
49
+ # :recursive:: after the clone is created, initialize all submodules within, using their default settings.
50
+ # :depth:: the number of commits back to pull
51
+ #
52
+ # TODO - make this work with SSH password or auth_key
53
+ #
54
+ def clone(repository, name, opts = {})
55
+ @path = opts[:path] || '.'
56
+ clone_dir = opts[:path] ? File.join(@path, name) : name
57
+
58
+ arr_opts = []
59
+ arr_opts << "--bare" if opts[:bare]
60
+ arr_opts << "--recursive" if opts[:recursive]
61
+ arr_opts << "-o" << opts[:remote] if opts[:remote]
62
+ arr_opts << "--depth" << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0
63
+ arr_opts << "--config" << opts[:config] if opts[:config]
64
+
65
+ arr_opts << '--'
66
+ arr_opts << repository
67
+ arr_opts << clone_dir
68
+
69
+ command('clone', arr_opts)
70
+
71
+ opts[:bare] ? {:repository => clone_dir} : {:working_directory => clone_dir}
72
+ end
73
+
74
+
75
+ ## READ COMMANDS ##
76
+
77
+ def log_commits(opts={})
78
+ arr_opts = log_common_options(opts)
79
+
80
+ arr_opts << '--pretty=oneline'
81
+
82
+ arr_opts += log_path_options(opts)
83
+
84
+ command_lines('log', arr_opts, true).map { |l| l.split.first }
85
+ end
86
+
87
+ def full_log_commits(opts={})
88
+ arr_opts = log_common_options(opts)
89
+
90
+ arr_opts << '--pretty=raw'
91
+ arr_opts << "--skip=#{opts[:skip]}" if opts[:skip]
92
+
93
+ arr_opts += log_path_options(opts)
94
+
95
+ full_log = command_lines('log', arr_opts, true)
96
+
97
+ process_commit_log_data(full_log)
98
+ end
99
+
100
+ def revparse(string)
101
+ return string if string =~ /[A-Fa-f0-9]{40}/ # passing in a sha - just no-op it
102
+ rev = ['head', 'remotes', 'tags'].map do |d|
103
+ File.join(@git_dir, 'refs', d, string)
104
+ end.find do |path|
105
+ File.file?(path)
106
+ end
107
+ return File.read(rev).chomp if rev
108
+ command('rev-parse', string)
109
+ end
110
+
111
+ def namerev(string)
112
+ command('name-rev', string).split[1]
113
+ end
114
+
115
+ def object_type(sha)
116
+ command('cat-file', ['-t', sha])
117
+ end
118
+
119
+ def object_size(sha)
120
+ command('cat-file', ['-s', sha]).to_i
121
+ end
122
+
123
+ # returns useful array of raw commit object data
124
+ def commit_data(sha)
125
+ sha = sha.to_s
126
+ cdata = command_lines('cat-file', ['commit', sha])
127
+ process_commit_data(cdata, sha, 0)
128
+ end
129
+
130
+ def process_commit_data(data, sha = nil, indent = 4)
131
+ hsh = {
132
+ 'sha' => sha,
133
+ 'message' => '',
134
+ 'parent' => []
135
+ }
136
+
137
+ loop do
138
+ key, *value = data.shift.split
139
+
140
+ break if key.nil?
141
+
142
+ if key == 'parent'
143
+ hsh['parent'] << value.join(' ')
144
+ else
145
+ hsh[key] = value.join(' ')
146
+ end
147
+ end
148
+
149
+ hsh['message'] = data.collect {|line| line[indent..-1]}.join("\n") + "\n"
150
+
151
+ return hsh
152
+ end
153
+
154
+ def process_commit_log_data(data)
155
+ in_message = false
156
+
157
+ hsh_array = []
158
+
159
+ hsh = nil
160
+
161
+ data.each do |line|
162
+ line = line.chomp
163
+
164
+ if line[0].nil?
165
+ in_message = !in_message
166
+ next
167
+ end
168
+
169
+ if in_message
170
+ hsh['message'] << "#{line[4..-1]}\n"
171
+ next
172
+ end
173
+
174
+ key, *value = line.split
175
+ value = value.join(' ')
176
+
177
+ case key
178
+ when 'commit'
179
+ hsh_array << hsh if hsh
180
+ hsh = {'sha' => value, 'message' => '', 'parent' => []}
181
+ when 'parent'
182
+ hsh['parent'] << value
183
+ else
184
+ hsh[key] = value
185
+ end
186
+ end
187
+
188
+ hsh_array << hsh if hsh
189
+
190
+ return hsh_array
191
+ end
192
+
193
+ def object_contents(sha, &block)
194
+ command('cat-file', ['-p', sha], &block)
195
+ end
196
+
197
+ def ls_tree(sha)
198
+ data = {'blob' => {}, 'tree' => {}}
199
+
200
+ command_lines('ls-tree', sha).each do |line|
201
+ (info, filenm) = line.split("\t")
202
+ (mode, type, sha) = info.split
203
+ data[type][filenm] = {:mode => mode, :sha => sha}
204
+ end
205
+
206
+ data
207
+ end
208
+
209
+ def mv(file1, file2)
210
+ command_lines('mv', ['--', file1, file2])
211
+ end
212
+
213
+ def full_tree(sha)
214
+ command_lines('ls-tree', ['-r', sha])
215
+ end
216
+
217
+ def tree_depth(sha)
218
+ full_tree(sha).size
219
+ end
220
+
221
+ def change_head_branch(branch_name)
222
+ command('symbolic-ref', ['HEAD', "refs/heads/#{branch_name}"])
223
+ end
224
+
225
+ def branches_all
226
+ arr = []
227
+ command_lines('branch', '-a').each do |b|
228
+ current = (b[0, 2] == '* ')
229
+ arr << [b.gsub('* ', '').strip, current]
230
+ end
231
+ arr
232
+ end
233
+
234
+ def list_files(ref_dir)
235
+ dir = File.join(@git_dir, 'refs', ref_dir)
236
+ files = []
237
+ Dir.chdir(dir) { files = Dir.glob('**/*').select { |f| File.file?(f) } } rescue nil
238
+ files
239
+ end
240
+
241
+ def branch_current
242
+ branches_all.select { |b| b[1] }.first[0] rescue nil
243
+ end
244
+
245
+
246
+ # returns hash
247
+ # [tree-ish] = [[line_no, match], [line_no, match2]]
248
+ # [tree-ish] = [[line_no, match], [line_no, match2]]
249
+ def grep(string, opts = {})
250
+ opts[:object] ||= 'HEAD'
251
+
252
+ grep_opts = ['-n']
253
+ grep_opts << '-i' if opts[:ignore_case]
254
+ grep_opts << '-v' if opts[:invert_match]
255
+ grep_opts << '-e'
256
+ grep_opts << string
257
+ grep_opts << opts[:object] if opts[:object].is_a?(String)
258
+ grep_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
259
+
260
+ hsh = {}
261
+ command_lines('grep', grep_opts).each do |line|
262
+ if m = /(.*)\:(\d+)\:(.*)/.match(line)
263
+ hsh[m[1]] ||= []
264
+ hsh[m[1]] << [m[2].to_i, m[3]]
265
+ end
266
+ end
267
+ hsh
268
+ end
269
+
270
+ def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
271
+ diff_opts = ['-p']
272
+ diff_opts << obj1
273
+ diff_opts << obj2 if obj2.is_a?(String)
274
+ diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
275
+
276
+ command('diff', diff_opts)
277
+ end
278
+
279
+ def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
280
+ diff_opts = ['--numstat']
281
+ diff_opts << obj1
282
+ diff_opts << obj2 if obj2.is_a?(String)
283
+ diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
284
+
285
+ hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}}
286
+
287
+ command_lines('diff', diff_opts).each do |file|
288
+ (insertions, deletions, filename) = file.split("\t")
289
+ hsh[:total][:insertions] += insertions.to_i
290
+ hsh[:total][:deletions] += deletions.to_i
291
+ hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions])
292
+ hsh[:total][:files] += 1
293
+ hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i}
294
+ end
295
+
296
+ hsh
297
+ end
298
+
299
+ # compares the index and the working directory
300
+ def diff_files
301
+ diff_as_hash('diff-files')
302
+ end
303
+
304
+ # compares the index and the repository
305
+ def diff_index(treeish)
306
+ diff_as_hash('diff-index', treeish)
307
+ end
308
+
309
+ def ls_files(location=nil)
310
+ hsh = {}
311
+ command_lines('ls-files', ['--stage', location]).each do |line|
312
+ (info, file) = line.split("\t")
313
+ (mode, sha, stage) = info.split
314
+ file = eval(file) if file =~ /^\".*\"$/ # This takes care of quoted strings returned from git
315
+ hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
316
+ end
317
+ hsh
318
+ end
319
+
320
+ def ls_remote(location=nil)
321
+ Hash.new{ |h,k| h[k] = {} }.tap do |hsh|
322
+ command_lines('ls-remote', [location], false).each do |line|
323
+ (sha, info) = line.split("\t")
324
+ (ref, type, name) = info.split('/', 3)
325
+ type ||= 'head'
326
+ type = 'branches' if type == 'heads'
327
+ value = {:ref => ref, :sha => sha}
328
+ hsh[type].update( name.nil? ? value : { name => value })
329
+ end
330
+ end
331
+ end
332
+
333
+ def ignored_files
334
+ command_lines('ls-files', ['--others', '-i', '--exclude-standard'])
335
+ end
336
+
337
+
338
+ def config_remote(name)
339
+ hsh = {}
340
+ config_list.each do |key, value|
341
+ if /remote.#{name}/.match(key)
342
+ hsh[key.gsub("remote.#{name}.", '')] = value
343
+ end
344
+ end
345
+ hsh
346
+ end
347
+
348
+ def config_get(name)
349
+ do_get = lambda do |path|
350
+ command('config', ['--get', name])
351
+ end
352
+
353
+ if @git_dir
354
+ Dir.chdir(@git_dir, &do_get)
355
+ else
356
+ build_list.call
357
+ end
358
+ end
359
+
360
+ def global_config_get(name)
361
+ command('config', ['--global', '--get', name], false)
362
+ end
363
+
364
+ def config_list
365
+ build_list = lambda do |path|
366
+ parse_config_list command_lines('config', ['--list'])
367
+ end
368
+
369
+ if @git_dir
370
+ Dir.chdir(@git_dir, &build_list)
371
+ else
372
+ build_list.call
373
+ end
374
+ end
375
+
376
+ def global_config_list
377
+ parse_config_list command_lines('config', ['--global', '--list'], false)
378
+ end
379
+
380
+ def parse_config_list(lines)
381
+ hsh = {}
382
+ lines.each do |line|
383
+ (key, *values) = line.split('=')
384
+ hsh[key] = values.join('=')
385
+ end
386
+ hsh
387
+ end
388
+
389
+ def parse_config(file)
390
+ parse_config_list command_lines('config', ['--list', '--file', file], false)
391
+ end
392
+
393
+ ## WRITE COMMANDS ##
394
+
395
+ def config_set(name, value)
396
+ command('config', [name, value])
397
+ end
398
+
399
+ def global_config_set(name, value)
400
+ command('config', ['--global', name, value], false)
401
+ end
402
+
403
+ # updates the repository index using the workig dorectory content
404
+ #
405
+ # lib.add('path/to/file')
406
+ # lib.add(['path/to/file1','path/to/file2'])
407
+ # lib.add(:all => true)
408
+ #
409
+ # options:
410
+ # :all => true
411
+ # :force => true
412
+ #
413
+ # @param [String,Array] paths files paths to be added to the repository
414
+ # @param [Hash] options
415
+ def add(paths='.',options={})
416
+ arr_opts = []
417
+
418
+ arr_opts << '--all' if options[:all]
419
+ arr_opts << '--force' if options[:force]
420
+
421
+ arr_opts << '--'
422
+
423
+ arr_opts << paths
424
+
425
+ arr_opts.flatten!
426
+
427
+ command('add', arr_opts)
428
+ end
429
+
430
+ def remove(path = '.', opts = {})
431
+ arr_opts = ['-f'] # overrides the up-to-date check by default
432
+ arr_opts << ['-r'] if opts[:recursive]
433
+ arr_opts << '--'
434
+ if path.is_a?(Array)
435
+ arr_opts += path
436
+ else
437
+ arr_opts << path
438
+ end
439
+
440
+ command('rm', arr_opts)
441
+ end
442
+
443
+ def commit(message, opts = {})
444
+ arr_opts = []
445
+ arr_opts << "--message=#{message}" if message
446
+ arr_opts << '--amend' << '--no-edit' if opts[:amend]
447
+ arr_opts << '--all' if opts[:add_all] || opts[:all]
448
+ arr_opts << '--allow-empty' if opts[:allow_empty]
449
+ arr_opts << "--author=#{opts[:author]}" if opts[:author]
450
+
451
+ command('commit', arr_opts)
452
+ end
453
+
454
+ def reset(commit, opts = {})
455
+ arr_opts = []
456
+ arr_opts << '--hard' if opts[:hard]
457
+ arr_opts << commit if commit
458
+ command('reset', arr_opts)
459
+ end
460
+
461
+ def clean(opts = {})
462
+ arr_opts = []
463
+ arr_opts << '--force' if opts[:force]
464
+ arr_opts << '-d' if opts[:d]
465
+ arr_opts << '-x' if opts[:x]
466
+
467
+ command('clean', arr_opts)
468
+ end
469
+
470
+ def revert(commitish, opts = {})
471
+ # Forcing --no-edit as default since it's not an interactive session.
472
+ opts = {:no_edit => true}.merge(opts)
473
+
474
+ arr_opts = []
475
+ arr_opts << '--no-edit' if opts[:no_edit]
476
+ arr_opts << commitish
477
+
478
+ command('revert', arr_opts)
479
+ end
480
+
481
+ def apply(patch_file)
482
+ arr_opts = []
483
+ arr_opts << '--' << patch_file if patch_file
484
+ command('apply', arr_opts)
485
+ end
486
+
487
+ def apply_mail(patch_file)
488
+ arr_opts = []
489
+ arr_opts << '--' << patch_file if patch_file
490
+ command('am', arr_opts)
491
+ end
492
+
493
+ def stashes_all
494
+ arr = []
495
+ filename = File.join(@git_dir, 'logs/refs/stash')
496
+ if File.exist?(filename)
497
+ File.open(filename).each_with_index { |line, i|
498
+ m = line.match(/:(.*)$/)
499
+ arr << [i, m[1].strip]
500
+ }
501
+ end
502
+ arr
503
+ end
504
+
505
+ def stash_save(message)
506
+ output = command('stash save', ['--', message])
507
+ output =~ /HEAD is now at/
508
+ end
509
+
510
+ def stash_apply(id = nil)
511
+ if id
512
+ command('stash apply', [id])
513
+ else
514
+ command('stash apply')
515
+ end
516
+ end
517
+
518
+ def stash_clear
519
+ command('stash clear')
520
+ end
521
+
522
+ def stash_list
523
+ command('stash list')
524
+ end
525
+
526
+ def branch_new(branch)
527
+ command('branch', branch)
528
+ end
529
+
530
+ def branch_delete(branch)
531
+ command('branch', ['-D', branch])
532
+ end
533
+
534
+ def checkout(branch, opts = {})
535
+ arr_opts = []
536
+ arr_opts << '-f' if opts[:force]
537
+ arr_opts << '-b' << opts[:new_branch] if opts[:new_branch]
538
+ arr_opts << branch
539
+
540
+ command('checkout', arr_opts)
541
+ end
542
+
543
+ def checkout_file(version, file)
544
+ arr_opts = []
545
+ arr_opts << version
546
+ arr_opts << file
547
+ command('checkout', arr_opts)
548
+ end
549
+
550
+ def merge(branch, message = nil)
551
+ arr_opts = []
552
+ arr_opts << '-m' << message if message
553
+ arr_opts += [branch]
554
+ command('merge', arr_opts)
555
+ end
556
+
557
+ def unmerged
558
+ unmerged = []
559
+ command_lines('diff', ["--cached"]).each do |line|
560
+ unmerged << $1 if line =~ /^\* Unmerged path (.*)/
561
+ end
562
+ unmerged
563
+ end
564
+
565
+ def conflicts # :yields: file, your, their
566
+ self.unmerged.each do |f|
567
+ your = Tempfile.new("YOUR-#{File.basename(f)}").path
568
+ command('show', ":2:#{f}", true, "> #{escape your}")
569
+
570
+ their = Tempfile.new("THEIR-#{File.basename(f)}").path
571
+ command('show', ":3:#{f}", true, "> #{escape their}")
572
+ yield(f, your, their)
573
+ end
574
+ end
575
+
576
+ def remote_add(name, url, opts = {})
577
+ arr_opts = ['add']
578
+ arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
579
+ arr_opts << '-t' << opts[:track] if opts[:track]
580
+ arr_opts << '--'
581
+ arr_opts << name
582
+ arr_opts << url
583
+
584
+ command('remote', arr_opts)
585
+ end
586
+
587
+ def remote_remove(name)
588
+ command('remote', ['rm', name])
589
+ end
590
+
591
+ def remotes
592
+ command_lines('remote')
593
+ end
594
+
595
+ def tags
596
+ command_lines('tag')
597
+ end
598
+
599
+ def tag(name, *opts)
600
+ target = opts[0].instance_of?(String) ? opts[0] : nil
601
+
602
+ opts = opts.last.instance_of?(Hash) ? opts.last : {}
603
+
604
+ if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message])
605
+ raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]."
606
+ end
607
+
608
+ arr_opts = []
609
+
610
+ arr_opts << '-f' if opts[:force] || opts[:f]
611
+ arr_opts << '-a' if opts[:a] || opts[:annotate]
612
+ arr_opts << '-s' if opts[:s] || opts[:sign]
613
+ arr_opts << '-d' if opts[:d] || opts[:delete]
614
+ arr_opts << name
615
+ arr_opts << target if target
616
+ arr_opts << "-m #{opts[:m] || opts[:message]}" if opts[:m] || opts[:message]
617
+
618
+ command('tag', arr_opts)
619
+ end
620
+
621
+
622
+ def fetch(remote, opts)
623
+ arr_opts = [remote]
624
+ arr_opts << '--tags' if opts[:t] || opts[:tags]
625
+
626
+ command('fetch', arr_opts)
627
+ end
628
+
629
+ def push(remote, branch = 'master', opts = {})
630
+ # Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature.
631
+ opts = {:tags => opts} if [true, false].include?(opts)
632
+
633
+ arr_opts = []
634
+ arr_opts << '--force' if opts[:force] || opts[:f]
635
+ arr_opts << remote
636
+
637
+ command('push', arr_opts + [branch])
638
+ command('push', ['--tags'] + arr_opts) if opts[:tags]
639
+ end
640
+
641
+ def pull(remote='origin', branch='master')
642
+ command('pull', [remote, branch])
643
+ end
644
+
645
+ def tag_sha(tag_name)
646
+ head = File.join(@git_dir, 'refs', 'tags', tag_name)
647
+ return File.read(head).chomp if File.exists?(head)
648
+
649
+ command('show-ref', ['--tags', '-s', tag_name])
650
+ end
651
+
652
+ def repack
653
+ command('repack', ['-a', '-d'])
654
+ end
655
+
656
+ def gc
657
+ command('gc', ['--prune', '--aggressive', '--auto'])
658
+ end
659
+
660
+ # reads a tree into the current index file
661
+ def read_tree(treeish, opts = {})
662
+ arr_opts = []
663
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
664
+ arr_opts += [treeish]
665
+ command('read-tree', arr_opts)
666
+ end
667
+
668
+ def write_tree
669
+ command('write-tree')
670
+ end
671
+
672
+ def commit_tree(tree, opts = {})
673
+ opts[:message] ||= "commit tree #{tree}"
674
+ t = Tempfile.new('commit-message')
675
+ t.write(opts[:message])
676
+ t.close
677
+
678
+ arr_opts = []
679
+ arr_opts << tree
680
+ arr_opts << '-p' << opts[:parent] if opts[:parent]
681
+ arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents]
682
+ command('commit-tree', arr_opts, true, "< #{escape t.path}")
683
+ end
684
+
685
+ def update_ref(branch, commit)
686
+ command('update-ref', [branch, commit])
687
+ end
688
+
689
+ def checkout_index(opts = {})
690
+ arr_opts = []
691
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
692
+ arr_opts << "--force" if opts[:force]
693
+ arr_opts << "--all" if opts[:all]
694
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
695
+
696
+ command('checkout-index', arr_opts)
697
+ end
698
+
699
+ # creates an archive file
700
+ #
701
+ # options
702
+ # :format (zip, tar)
703
+ # :prefix
704
+ # :remote
705
+ # :path
706
+ def archive(sha, file = nil, opts = {})
707
+ opts[:format] ||= 'zip'
708
+
709
+ if opts[:format] == 'tgz'
710
+ opts[:format] = 'tar'
711
+ opts[:add_gzip] = true
712
+ end
713
+
714
+ file ||= Tempfile.new('archive').path
715
+
716
+ arr_opts = []
717
+ arr_opts << "--format=#{opts[:format]}" if opts[:format]
718
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
719
+ arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
720
+ arr_opts << sha
721
+ arr_opts << '--' << opts[:path] if opts[:path]
722
+ command('archive', arr_opts, true, (opts[:add_gzip] ? '| gzip' : '') + " > #{escape file}")
723
+ return file
724
+ end
725
+
726
+ # returns the current version of git, as an Array of Fixnums.
727
+ def current_command_version
728
+ output = command('version', [], false)
729
+ version = output[/\d+\.\d+(\.\d+)+/]
730
+ version.split('.').collect {|i| i.to_i}
731
+ end
732
+
733
+ def required_command_version
734
+ [1, 6]
735
+ end
736
+
737
+ def meets_required_version?
738
+ (self.current_command_version <=> self.required_command_version) >= 0
739
+ end
740
+
741
+
742
+ private
743
+
744
+ def command_lines(cmd, opts = [], chdir = true, redirect = '')
745
+ command(cmd, opts, chdir).split("\n")
746
+ end
747
+
748
+ def command(cmd, opts = [], chdir = true, redirect = '', &block)
749
+ ENV['GIT_DIR'] = @git_dir
750
+ ENV['GIT_WORK_TREE'] = @git_work_dir
751
+ ENV['GIT_INDEX_FILE'] = @git_index_file
752
+
753
+ path = @git_work_dir || @git_dir || @path
754
+
755
+ opts = [opts].flatten.map {|s| escape(s) }.join(' ')
756
+
757
+ git_cmd = "git #{cmd} #{opts} #{redirect} 2>&1"
758
+
759
+ out = nil
760
+ if chdir && (Dir.getwd != path)
761
+ Dir.chdir(path) { out = run_command(git_cmd, &block) }
762
+ else
763
+
764
+ out = run_command(git_cmd, &block)
765
+ end
766
+
767
+ if @logger
768
+ @logger.info(git_cmd)
769
+ @logger.debug(out)
770
+ end
771
+
772
+ if $?.exitstatus > 0
773
+ if $?.exitstatus == 1 && out == ''
774
+ return ''
775
+ end
776
+ raise Git::GitExecuteError.new(git_cmd + ':' + out.to_s)
777
+ end
778
+ out
779
+ end
780
+
781
+ # Takes the diff command line output (as Array) and parse it into a Hash
782
+ #
783
+ # @param [String] diff_command the diff commadn to be used
784
+ # @param [Array] opts the diff options to be used
785
+ # @return [Hash] the diff as Hash
786
+ def diff_as_hash(diff_command, opts=[])
787
+ command_lines(diff_command, opts).inject({}) do |memo, line|
788
+ info, file = line.split("\t")
789
+ mode_src, mode_dest, sha_src, sha_dest, type = info.split
790
+
791
+ memo[file] = {
792
+ :mode_index => mode_dest,
793
+ :mode_repo => mode_src.to_s[1, 7],
794
+ :path => file,
795
+ :sha_repo => sha_src,
796
+ :sha_index => sha_dest,
797
+ :type => type
798
+ }
799
+
800
+ memo
801
+ end
802
+ end
803
+
804
+ # Returns an array holding the common options for the log commands
805
+ #
806
+ # @param [Hash] opts the given options
807
+ # @return [Array] the set of common options that the log command will use
808
+ def log_common_options(opts)
809
+ arr_opts = []
810
+
811
+ arr_opts << "-#{opts[:count]}" if opts[:count]
812
+ arr_opts << "--no-color"
813
+ arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
814
+ arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
815
+ arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
816
+ arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
817
+ arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
818
+
819
+ arr_opts
820
+ end
821
+
822
+ # Retrurns an array holding path options for the log commands
823
+ #
824
+ # @param [Hash] opts the given options
825
+ # @return [Array] the set of path options that the log command will use
826
+ def log_path_options(opts)
827
+ arr_opts = []
828
+
829
+ arr_opts << opts[:object] if opts[:object].is_a? String
830
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter]
831
+
832
+ arr_opts
833
+ end
834
+
835
+ def run_command(git_cmd, &block)
836
+ if block_given?
837
+ IO.popen(git_cmd, &block)
838
+ else
839
+ `#{git_cmd}`.chomp
840
+ end
841
+ end
842
+
843
+ def escape(s)
844
+ "'#{s && s.to_s.gsub('\'','\'"\'"\'')}'"
845
+ end
846
+
847
+ end
848
+ end