clc-git 1.2.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,844 @@
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
+
321
+ def ignored_files
322
+ command_lines('ls-files', ['--others', '-i', '--exclude-standard'])
323
+ end
324
+
325
+
326
+ def config_remote(name)
327
+ hsh = {}
328
+ config_list.each do |key, value|
329
+ if /remote.#{name}/.match(key)
330
+ hsh[key.gsub("remote.#{name}.", '')] = value
331
+ end
332
+ end
333
+ hsh
334
+ end
335
+
336
+ def config_get(name)
337
+ do_get = lambda do |path|
338
+ command('config', ['--get', name])
339
+ end
340
+
341
+ if @git_dir
342
+ Dir.chdir(@git_dir, &do_get)
343
+ else
344
+ build_list.call
345
+ end
346
+ end
347
+
348
+ def global_config_get(name)
349
+ command('config', ['--global', '--get', name], false)
350
+ end
351
+
352
+ def config_list
353
+ build_list = lambda do |path|
354
+ parse_config_list command_lines('config', ['--list'])
355
+ end
356
+
357
+ if @git_dir
358
+ Dir.chdir(@git_dir, &build_list)
359
+ else
360
+ build_list.call
361
+ end
362
+ end
363
+
364
+ def global_config_list
365
+ parse_config_list command_lines('config', ['--global', '--list'], false)
366
+ end
367
+
368
+ def parse_config_list(lines)
369
+ hsh = {}
370
+ lines.each do |line|
371
+ (key, *values) = line.split('=')
372
+ hsh[key] = values.join('=')
373
+ end
374
+ hsh
375
+ end
376
+
377
+ def parse_config(file)
378
+ parse_config_list command_lines('config', ['--list', '--file', file], false)
379
+ end
380
+
381
+ ## WRITE COMMANDS ##
382
+
383
+ def config_set(name, value)
384
+ command('config', [name, value])
385
+ end
386
+
387
+ def global_config_set(name, value)
388
+ command('config', ['--global', name, value], false)
389
+ end
390
+
391
+ # updates the repository index using the workig dorectory content
392
+ #
393
+ # lib.add('path/to/file')
394
+ # lib.add(['path/to/file1','path/to/file2'])
395
+ # lib.add(:all => true)
396
+ #
397
+ # options:
398
+ # :all => true
399
+ # :force => true
400
+ #
401
+ # @param [String,Array] paths files paths to be added to the repository
402
+ # @param [Hash] options
403
+ def add(paths='.',options={})
404
+ arr_opts = []
405
+
406
+ arr_opts << '--all' if options[:all]
407
+ arr_opts << '--force' if options[:force]
408
+
409
+ arr_opts << '--'
410
+
411
+ arr_opts << paths
412
+
413
+ arr_opts.flatten!
414
+
415
+ command('add', arr_opts)
416
+ end
417
+
418
+ def remove(path = '.', opts = {})
419
+ arr_opts = ['-f'] # overrides the up-to-date check by default
420
+ arr_opts << ['-r'] if opts[:recursive]
421
+ arr_opts << '--'
422
+ if path.is_a?(Array)
423
+ arr_opts += path
424
+ else
425
+ arr_opts << path
426
+ end
427
+
428
+ command('rm', arr_opts)
429
+ end
430
+
431
+ def commit(message, opts = {})
432
+ arr_opts = []
433
+ arr_opts << "--message=#{message}" if message
434
+ arr_opts << '--amend' << '--no-edit' if opts[:amend]
435
+ arr_opts << '--all' if opts[:add_all] || opts[:all]
436
+ arr_opts << '--allow-empty' if opts[:allow_empty]
437
+ arr_opts << "--author=#{opts[:author]}" if opts[:author]
438
+
439
+ command('commit', arr_opts)
440
+ end
441
+
442
+ def reset(commit, opts = {})
443
+ arr_opts = []
444
+ arr_opts << '--hard' if opts[:hard]
445
+ arr_opts << commit if commit
446
+ command('reset', arr_opts)
447
+ end
448
+
449
+ def clean(opts = {})
450
+ arr_opts = []
451
+ arr_opts << '--force' if opts[:force]
452
+ arr_opts << '-d' if opts[:d]
453
+ arr_opts << '-x' if opts[:x]
454
+
455
+ command('clean', arr_opts)
456
+ end
457
+
458
+ def revert(commitish, opts = {})
459
+ # Forcing --no-edit as default since it's not an interactive session.
460
+ opts = {:no_edit => true}.merge(opts)
461
+
462
+ arr_opts = []
463
+ arr_opts << '--no-edit' if opts[:no_edit]
464
+ arr_opts << commitish
465
+
466
+ command('revert', arr_opts)
467
+ end
468
+
469
+ def apply(patch_file)
470
+ arr_opts = []
471
+ arr_opts << '--' << patch_file if patch_file
472
+ command('apply', arr_opts)
473
+ end
474
+
475
+ def apply_mail(patch_file)
476
+ arr_opts = []
477
+ arr_opts << '--' << patch_file if patch_file
478
+ command('am', arr_opts)
479
+ end
480
+
481
+ def stashes_all
482
+ arr = []
483
+ filename = File.join(@git_dir, 'logs/refs/stash')
484
+ if File.exist?(filename)
485
+ File.open(filename).each_with_index { |line, i|
486
+ m = line.match(/:(.*)$/)
487
+ arr << [i, m[1].strip]
488
+ }
489
+ end
490
+ arr
491
+ end
492
+
493
+ def stash_save(message)
494
+ output = command('stash save', ['--', message])
495
+ output =~ /HEAD is now at/
496
+ end
497
+
498
+ def stash_apply(id = nil)
499
+ if id
500
+ command('stash apply', [id])
501
+ else
502
+ command('stash apply')
503
+ end
504
+ end
505
+
506
+ def stash_clear
507
+ command('stash clear')
508
+ end
509
+
510
+ def stash_list
511
+ command('stash list')
512
+ end
513
+
514
+ def branch_new(branch)
515
+ command('branch', branch)
516
+ end
517
+
518
+ def branch_delete(branch)
519
+ command('branch', ['-D', branch])
520
+ end
521
+
522
+ def checkout(branch, opts = {})
523
+ arr_opts = []
524
+ arr_opts << '-f' if opts[:force]
525
+ arr_opts << '-b' << opts[:new_branch] if opts[:new_branch]
526
+ arr_opts << branch
527
+
528
+ command('checkout', arr_opts)
529
+ end
530
+
531
+ def checkout_file(version, file)
532
+ arr_opts = []
533
+ arr_opts << version
534
+ arr_opts << file
535
+ command('checkout', arr_opts)
536
+ end
537
+
538
+ def merge(branch, message = nil)
539
+ arr_opts = []
540
+ arr_opts << '-m' << message if message
541
+ arr_opts += [branch]
542
+ command('merge', arr_opts)
543
+ end
544
+
545
+ def unmerged
546
+ unmerged = []
547
+ command_lines('diff', ["--cached"]).each do |line|
548
+ unmerged << $1 if line =~ /^\* Unmerged path (.*)/
549
+ end
550
+ unmerged
551
+ end
552
+
553
+ def conflicts # :yields: file, your, their
554
+ self.unmerged.each do |f|
555
+ your = Tempfile.new("YOUR-#{File.basename(f)}").path
556
+ command('show', ":2:#{f}", true, "> #{escape your}")
557
+
558
+ their = Tempfile.new("THEIR-#{File.basename(f)}").path
559
+ command('show', ":3:#{f}", true, "> #{escape their}")
560
+ yield(f, your, their)
561
+ end
562
+ end
563
+
564
+ def remote_add(name, url, opts = {})
565
+ arr_opts = ['add']
566
+ arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
567
+ arr_opts << '-t' << opts[:track] if opts[:track]
568
+ arr_opts << '--'
569
+ arr_opts << name
570
+ arr_opts << url
571
+
572
+ command('remote', arr_opts)
573
+ end
574
+
575
+ def remote_remove(name)
576
+ command('remote', ['rm', name])
577
+ end
578
+
579
+ def remotes
580
+ command_lines('remote')
581
+ end
582
+
583
+ def tags
584
+ command_lines('tag')
585
+ end
586
+
587
+ def tag(name, *opts)
588
+ target = opts[0].instance_of?(String) ? opts[0] : nil
589
+
590
+ opts = opts.last.instance_of?(Hash) ? opts.last : {}
591
+
592
+ if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message])
593
+ raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]."
594
+ end
595
+
596
+ arr_opts = []
597
+
598
+ arr_opts << '-f' if opts[:force] || opts[:f]
599
+ arr_opts << '-a' if opts[:a] || opts[:annotate]
600
+ arr_opts << '-s' if opts[:s] || opts[:sign]
601
+ arr_opts << '-d' if opts[:d] || opts[:delete]
602
+ arr_opts << name
603
+ arr_opts << target if target
604
+ arr_opts << "-m #{opts[:m] || opts[:message]}" if opts[:m] || opts[:message]
605
+
606
+ command('tag', arr_opts)
607
+ end
608
+
609
+
610
+ def fetch(remote, opts)
611
+ arr_opts = [remote]
612
+ arr_opts << '--tags' if opts[:t] || opts[:tags]
613
+
614
+ command('fetch', arr_opts)
615
+ end
616
+
617
+ def push(remote, branch = 'master', opts = {})
618
+ # Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature.
619
+ opts = {:tags => opts} if [true, false].include?(opts)
620
+
621
+ arr_opts = []
622
+ arr_opts << '--force' if opts[:force] || opts[:f]
623
+ arr_opts << remote
624
+
625
+ command('push', arr_opts + [branch])
626
+ command('push', ['--tags'] + arr_opts) if opts[:tags]
627
+ end
628
+
629
+ def pull(remote='origin', branch='master')
630
+ command('pull', [remote, branch])
631
+ end
632
+
633
+ def tag_sha(tag_name)
634
+ head = File.join(@git_dir, 'refs', 'tags', tag_name)
635
+ return File.read(head).chomp if File.exist?(head)
636
+
637
+ command('show-ref', ['--tags', '-s', tag_name])
638
+ end
639
+
640
+ def repack
641
+ command('repack', ['-a', '-d'])
642
+ end
643
+
644
+ def gc
645
+ command('gc', ['--prune', '--aggressive', '--auto'])
646
+ end
647
+
648
+ # reads a tree into the current index file
649
+ def read_tree(treeish, opts = {})
650
+ arr_opts = []
651
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
652
+ arr_opts += [treeish]
653
+ command('read-tree', arr_opts)
654
+ end
655
+
656
+ def write_tree
657
+ command('write-tree')
658
+ end
659
+
660
+ def commit_tree(tree, opts = {})
661
+ opts[:message] ||= "commit tree #{tree}"
662
+ t = Tempfile.new('commit-message')
663
+ t.write(opts[:message])
664
+ t.close
665
+
666
+ arr_opts = []
667
+ arr_opts << tree
668
+ arr_opts << '-p' << opts[:parent] if opts[:parent]
669
+ arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents]
670
+ command('commit-tree', arr_opts, true, "< #{escape t.path}")
671
+ end
672
+
673
+ def update_ref(branch, commit)
674
+ command('update-ref', [branch, commit])
675
+ end
676
+
677
+ def checkout_index(opts = {})
678
+ arr_opts = []
679
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
680
+ arr_opts << "--force" if opts[:force]
681
+ arr_opts << "--all" if opts[:all]
682
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String
683
+
684
+ command('checkout-index', arr_opts)
685
+ end
686
+
687
+ # creates an archive file
688
+ #
689
+ # options
690
+ # :format (zip, tar)
691
+ # :prefix
692
+ # :remote
693
+ # :path
694
+ def archive(sha, file = nil, opts = {})
695
+ opts[:format] ||= 'zip'
696
+
697
+ if opts[:format] == 'tgz'
698
+ opts[:format] = 'tar'
699
+ opts[:add_gzip] = true
700
+ end
701
+
702
+ file ||= Tempfile.new('archive').path
703
+
704
+ arr_opts = []
705
+ arr_opts << "--format=#{opts[:format]}" if opts[:format]
706
+ arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
707
+ arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
708
+ arr_opts << sha
709
+ arr_opts << '--' << opts[:path] if opts[:path]
710
+ command('archive', arr_opts, true, (opts[:add_gzip] ? '| gzip' : '') + " > #{escape file}")
711
+ return file
712
+ end
713
+
714
+ # returns the current version of git, as an Array of Fixnums.
715
+ def current_command_version
716
+ output = command('version', [], false)
717
+ version = output[/\d+\.\d+(\.\d+)+/]
718
+ version.split('.').collect {|i| i.to_i}
719
+ end
720
+
721
+ def required_command_version
722
+ [1, 6]
723
+ end
724
+
725
+ def meets_required_version?
726
+ (self.current_command_version <=> self.required_command_version) >= 0
727
+ end
728
+
729
+
730
+ private
731
+
732
+ def command_lines(cmd, opts = [], chdir = true, redirect = '')
733
+ command(cmd, opts, chdir).split("\n")
734
+ end
735
+
736
+ def command(cmd, opts = [], chdir = true, redirect = '', &block)
737
+ old_git_dir = ENV['GIT_DIR']
738
+ old_git_work_dir = ENV['GIT_WORK_TREE']
739
+ old_git_index_file = ENV['GIT_INDEX_FILE']
740
+
741
+ ENV['GIT_DIR'] = @git_dir
742
+ ENV['GIT_WORK_TREE'] = @git_work_dir
743
+ ENV['GIT_INDEX_FILE'] = @git_index_file
744
+
745
+ path = @git_work_dir || @git_dir || @path
746
+
747
+ opts = [opts].flatten.map {|s| escape(s) }.join(' ')
748
+
749
+ git_cmd = "git #{cmd} #{opts} #{redirect} 2>&1"
750
+
751
+ output = nil
752
+
753
+ if chdir && (Dir.getwd != path)
754
+ Dir.chdir(path) { output = run_command(git_cmd, &block) }
755
+ else
756
+ output = run_command(git_cmd, &block)
757
+ end
758
+
759
+ if @logger
760
+ @logger.info(git_cmd)
761
+ @logger.debug(output)
762
+ end
763
+
764
+ if $?.exitstatus > 1 || ($?.exitstatus == 1 && output != '')
765
+ raise Git::GitExecuteError.new(git_cmd + ':' + output.to_s)
766
+ end
767
+
768
+ return output
769
+ ensure
770
+ ENV['GIT_DIR'] = old_git_dir
771
+ ENV['GIT_WORK_TREE'] = old_git_work_dir
772
+ ENV['GIT_INDEX_FILE'] = old_git_index_file
773
+ end
774
+
775
+ # Takes the diff command line output (as Array) and parse it into a Hash
776
+ #
777
+ # @param [String] diff_command the diff commadn to be used
778
+ # @param [Array] opts the diff options to be used
779
+ # @return [Hash] the diff as Hash
780
+ def diff_as_hash(diff_command, opts=[])
781
+ command_lines(diff_command, opts).inject({}) do |memo, line|
782
+ info, file = line.split("\t")
783
+ mode_src, mode_dest, sha_src, sha_dest, type = info.split
784
+
785
+ memo[file] = {
786
+ :mode_index => mode_dest,
787
+ :mode_repo => mode_src.to_s[1, 7],
788
+ :path => file,
789
+ :sha_repo => sha_src,
790
+ :sha_index => sha_dest,
791
+ :type => type
792
+ }
793
+
794
+ memo
795
+ end
796
+ end
797
+
798
+ # Returns an array holding the common options for the log commands
799
+ #
800
+ # @param [Hash] opts the given options
801
+ # @return [Array] the set of common options that the log command will use
802
+ def log_common_options(opts)
803
+ arr_opts = []
804
+
805
+ arr_opts << "-#{opts[:count]}" if opts[:count]
806
+ arr_opts << "--no-color"
807
+ arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
808
+ arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
809
+ arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
810
+ arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
811
+ arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
812
+
813
+ arr_opts
814
+ end
815
+
816
+ # Retrurns an array holding path options for the log commands
817
+ #
818
+ # @param [Hash] opts the given options
819
+ # @return [Array] the set of path options that the log command will use
820
+ def log_path_options(opts)
821
+ arr_opts = []
822
+
823
+ arr_opts << opts[:object] if opts[:object].is_a? String
824
+ arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter]
825
+
826
+ arr_opts
827
+ end
828
+
829
+ def run_command(git_cmd, &block)
830
+ return IO.popen(git_cmd, &block) if block_given?
831
+
832
+ `#{git_cmd}`.chomp
833
+ end
834
+
835
+ def escape(s)
836
+ return "'#{s && s.to_s.gsub('\'','\'"\'"\'')}'" if RUBY_PLATFORM !~ /mingw|mswin/
837
+
838
+ # Keeping the old escape format for windows users
839
+ escaped = s.to_s.gsub('\'', '\'\\\'\'')
840
+ return %Q{"#{escaped}"}
841
+ end
842
+
843
+ end
844
+ end