spikegrobstein-git 1.2.7

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