changed logging to be far more efficient if you're accessing all the commit objects
[rubygit.git] / lib / git / lib.rb
blob0d441833589d1ea75c021053a0094a73fab2813c
1 require 'tempfile'
3 module Git
4   
5   class GitExecuteError < StandardError 
6   end
7   
8   class Lib
9       
10     @git_dir = nil
11     @git_index_file = nil
12     @git_work_dir = nil
13     @path = nil
14         
15     def initialize(base = nil)
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     end
26     
27     def init
28       command('init')
29     end
30     
31     # tries to clone the given repo
32     #
33     # returns {:repository} (if bare)
34     #         {:working_directory} otherwise
35     #
36     # accepts options:
37     #  :remote - name of remote (rather than 'origin')
38     #  :bare   - no working directory
39     # 
40     # TODO - make this work with SSH password or auth_key
41     #
42     def clone(repository, name, opts = {})
43       @path = opts[:path] || '.'
44       opts[:path] ? clone_dir = File.join(@path, name) : clone_dir = name
45       
46       arr_opts = []
47       arr_opts << "--bare" if opts[:bare]
48       arr_opts << "-o #{opts[:remote]}" if opts[:remote]
49       arr_opts << repository
50       arr_opts << clone_dir
51       
52       command('clone', arr_opts)
53       
54       opts[:bare] ? {:repository => clone_dir} : {:working_directory => clone_dir}
55     end
56     
57     
58     ## READ COMMANDS ##
59     
60     
61     def log_commits(opts = {})
62       arr_opts = ['--pretty=oneline']
63       arr_opts << "-#{opts[:count]}" if opts[:count]
64       arr_opts << "--since=\"#{opts[:since]}\"" if opts[:since].is_a? String
65       arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
66       arr_opts << opts[:object] if opts[:object].is_a? String
67       arr_opts << '-- ' + opts[:path_limiter] if opts[:path_limiter].is_a? String
68       
69       command_lines('log', arr_opts, true).map { |l| l.split.first }
70     end
71     
72     def full_log_commits(opts = {})
73       arr_opts = ['--pretty=raw']
74       arr_opts << "-#{opts[:count]}" if opts[:count]
75       arr_opts << "--since=\"#{opts[:since]}\"" if opts[:since].is_a? String
76       arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
77       arr_opts << opts[:object] if opts[:object].is_a? String
78       arr_opts << '-- ' + opts[:path_limiter] if opts[:path_limiter].is_a? String
79       
80       full_log = command_lines('log', arr_opts, true)
81       process_commit_data(full_log)
82     end
83     
84     def revparse(string)
85       command('rev-parse', string)
86     end
87     
88     def namerev(string)
89       command('name-rev', string).split[1]
90     end
91     
92     def object_type(sha)
93       command('cat-file', ['-t', sha])
94     end
95     
96     def object_size(sha)
97       command('cat-file', ['-s', sha]).to_i
98     end
99     
100     # returns useful array of raw commit object data
101     def commit_data(sha)
102       sha = sha.to_s
103       cdata = command_lines('cat-file', ['commit', sha])
104       process_commit_data(cdata, sha)
105     end
106     
107     def process_commit_data(data, sha = nil)
108       in_message = false
109       
110       if sha
111         hsh = {'sha' => sha, 'message' => '', 'parent' => []}
112       else
113         hsh_array = []        
114       end
115     
116       data.each do |line|
117         if in_message && line != ''
118           hsh['message'] += line + "\n"
119         end
121         if (line != '') && !in_message
122           data = line.split
123           key = data.shift
124           value = data.join(' ')
125           if key == 'commit'
126             sha = value
127             hsh_array << hsh if hsh
128             hsh = {'sha' => sha, 'message' => '', 'parent' => []}
129           end
130           if key == 'parent'
131             hsh[key] << value
132           else
133             hsh[key] = value
134           end
135         elsif in_message && line == ''
136           in_message = false
137         else
138           in_message = true
139         end
140       end
141       
142       if hsh_array
143         hsh_array << hsh if hsh
144         hsh_array
145       else
146         hsh
147       end
148     end
149     
150     def object_contents(sha)
151       command('cat-file', ['-p', sha])
152     end
154     def ls_tree(sha)
155       data = {'blob' => {}, 'tree' => {}}
156       command_lines('ls-tree', sha.to_s).each do |line|
157         (info, filenm) = line.split("\t")
158         (mode, type, sha) = info.split
159         data[type][filenm] = {:mode => mode, :sha => sha}
160       end
161       data
162     end
164     def branches_all
165       arr = []
166       command_lines('branch', '-a').each do |b| 
167         current = false
168         current = true if b[0, 2] == '* '
169         arr << [b.gsub('* ', '').strip, current]
170       end
171       arr
172     end
174     def branch_current
175       branches_all.select { |b| b[1] }.first[0] rescue nil
176     end
179     # returns hash
180     # [tree-ish] = [[line_no, match], [line_no, match2]]
181     # [tree-ish] = [[line_no, match], [line_no, match2]]
182     def grep(string, opts = {})
183       opts[:object] = 'HEAD' if !opts[:object]
185       grep_opts = ['-n']
186       grep_opts << '-i' if opts[:ignore_case]
187       grep_opts << '-v' if opts[:invert_match]
188       grep_opts << "-e '#{string}'"
189       grep_opts << opts[:object] if opts[:object].is_a?(String)
190       grep_opts << ('-- ' + opts[:path_limiter]) if opts[:path_limiter].is_a? String
191       hsh = {}
192       command_lines('grep', grep_opts).each do |line|
193         if m = /(.*)\:(\d+)\:(.*)/.match(line)        
194           hsh[m[1]] ||= []
195           hsh[m[1]] << [m[2].to_i, m[3]] 
196         end
197       end
198       hsh
199     end
200     
201     def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
202       diff_opts = ['-p']
203       diff_opts << obj1
204       diff_opts << obj2 if obj2.is_a?(String)
205       diff_opts << ('-- ' + opts[:path_limiter]) if opts[:path_limiter].is_a? String
206       
207       command('diff', diff_opts)
208     end
209     
210     def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
211       diff_opts = ['--numstat']
212       diff_opts << obj1
213       diff_opts << obj2 if obj2.is_a?(String)
214       diff_opts << ('-- ' + opts[:path_limiter]) if opts[:path_limiter].is_a? String
215       
216       hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}}
217       
218       command_lines('diff', diff_opts).each do |file|
219         (insertions, deletions, filename) = file.split("\t")
220         hsh[:total][:insertions] += insertions.to_i
221         hsh[:total][:deletions] += deletions.to_i
222         hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions])
223         hsh[:total][:files] += 1
224         hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i}
225       end
226             
227       hsh
228     end
230     # compares the index and the working directory
231     def diff_files
232       hsh = {}
233       command_lines('diff-files').each do |line|
234         (info, file) = line.split("\t")
235         (mode_src, mode_dest, sha_src, sha_dest, type) = info.split
236         hsh[file] = {:path => file, :mode_file => mode_src.to_s[1, 7], :mode_index => mode_dest, 
237                       :sha_file => sha_src, :sha_index => sha_dest, :type => type}
238       end
239       hsh
240     end
241     
242     # compares the index and the repository
243     def diff_index(treeish)
244       hsh = {}
245       command_lines('diff-index', treeish).each do |line|
246         (info, file) = line.split("\t")
247         (mode_src, mode_dest, sha_src, sha_dest, type) = info.split
248         hsh[file] = {:path => file, :mode_repo => mode_src.to_s[1, 7], :mode_index => mode_dest, 
249                       :sha_repo => sha_src, :sha_index => sha_dest, :type => type}
250       end
251       hsh
252     end
253             
254     def ls_files
255       hsh = {}
256       command_lines('ls-files', '--stage').each do |line|
257         (info, file) = line.split("\t")
258         (mode, sha, stage) = info.split
259         hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
260       end
261       hsh
262     end
265     def config_remote(name)
266       hsh = {}
267       command_lines('config', ['--get-regexp', "remote.#{name}"]).each do |line|
268         (key, value) = line.split
269         hsh[key.gsub("remote.#{name}.", '')] = value
270       end
271       hsh
272     end
274     def config_get(name)
275       command('config', ['--get', name])
276     end
277     
278     def config_list
279       hsh = {}
280       command_lines('config', ['--list']).each do |line|
281         (key, value) = line.split('=')
282         hsh[key] = value
283       end
284       hsh
285     end
286     
287     ## WRITE COMMANDS ##
288         
289     def config_set(name, value)
290       command('config', [name, "'#{value}'"])
291     end
292           
293     def add(path = '.')
294       path = path.join(' ') if path.is_a?(Array)
295       command('add', path)
296     end
297     
298     def remove(path = '.', opts = {})
299       path = path.join(' ') if path.is_a?(Array)
301       arr_opts = ['-f']  # overrides the up-to-date check by default
302       arr_opts << ['-r'] if opts[:recursive]
303       arr_opts << path
305       command('rm', arr_opts)
306     end
308     def commit(message, opts = {})
309       arr_opts = ["-m '#{message}'"]
310       arr_opts << '-a' if opts[:add_all]
311       command('commit', arr_opts)
312     end
314     def reset(commit, opts = {})
315       arr_opts = []
316       arr_opts << '--hard' if opts[:hard]
317       arr_opts << commit.to_s if commit
318       command('reset', arr_opts)
319     end
322     def branch_new(branch)
323       command('branch', branch)
324     end
325     
326     def branch_delete(branch)
327       command('branch', ['-d', branch])
328     end
329     
330     def checkout(branch, opts = {})
331       arr_opts = []
332       arr_opts << '-f' if opts[:force]
333       arr_opts << branch.to_s
334       
335       command('checkout', arr_opts)
336     end
337     
338     def merge(branch, message = nil)      
339       arr_opts = []
340       arr_opts << ["-m '#{message}'"] if message
341       arr_opts << branch.to_a.join(' ')
342       command('merge', arr_opts)
343     end
344     
345     def remote_add(name, url, opts = {})
346       arr_opts = ['add']
347       arr_opts << '-f' if opts[:with_fetch]
348       arr_opts << name
349       arr_opts << url
350       
351       command('remote', arr_opts)
352     end
353     
354     # this is documented as such, but seems broken for some reason
355     # i'll try to get around it some other way later
356     def remote_remove(name)
357       command('remote', ['rm', name])
358     end
359     
360     def remotes
361       command_lines('remote')
362     end
364     def tags
365       command_lines('tag')
366     end
368     def tag(tag)
369       command('tag', tag)
370     end
372     
373     def fetch(remote)
374       command('fetch', remote.to_s)
375     end
376     
377     def push(remote, branch = 'master')
378       command('push', [remote.to_s, branch.to_s])
379     end
380     
381     def tag_sha(tag_name)
382       command('show-ref',  ['--tags', '-s', tag_name])
383     end  
384     
385     def repack
386       command('repack', ['-a', '-d'])
387     end
388     
389     # reads a tree into the current index file
390     def read_tree(treeish, opts = {})
391       arr_opts = []
392       arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
393       arr_opts << treeish.to_a.join(' ')
394       command('read-tree', arr_opts)
395     end
396     
397     def write_tree
398       command('write-tree')
399     end
400     
401     def commit_tree(tree, opts = {})
402       opts[:message] = "commit tree #{tree}" if !opts[:message]
403       t = Tempfile.new('commit-message') do |t|
404         t.write(opts[:message])
405       end
406       
407       arr_opts = []
408       arr_opts << tree
409       arr_opts << "-p #{opts[:parent]}" if opts[:parent]
410       opts[:parents].each { |p| arr_opts << "-p #{p.to_s}" } if opts[:parents]
411       arr_opts << "< #{t.path}"
412       command('commit-tree', arr_opts)
413     end
414     
415     def update_ref(branch, commit)
416       command('update-ref', [branch.to_s, commit.to_s])
417     end
418     
419     def checkout_index(opts = {})
420       arr_opts = []
421       arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
422       arr_opts << "--force" if opts[:force]
423       arr_opts << "--all" if opts[:all]
424       arr_opts << ('-- ' + opts[:path_limiter]) if opts[:path_limiter].is_a? String
425       command('checkout-index', arr_opts)
426     end
427     
428     # creates an archive file
429     #
430     # options
431     #  :format  (zip, tar)
432     #  :prefix
433     #  :remote
434     #  :path
435     def archive(sha, file = nil, opts = {})
436       opts[:format] = 'zip' if !opts[:format]
437       
438       if opts[:format] == 'tgz'
439         opts[:format] = 'tar' 
440         opts[:add_gzip] = true
441       end
442       
443       if !file
444         file = Tempfile.new('archive').path
445       end
446       
447       arr_opts = []
448       arr_opts << "--format=#{opts[:format]}" if opts[:format]
449       arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
450       arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
451       arr_opts << sha
452       arr_opts << opts[:path] if opts[:path]
453       arr_opts << '| gzip' if opts[:add_gzip]
454       arr_opts << "> #{file.to_s}"
455       command('archive', arr_opts)
456       return file
457     end
458     
459     private
460     
461     def command_lines(cmd, opts = {}, chdir = true)
462       command(cmd, opts, chdir).split("\n")
463     end
464     
465     def command(cmd, opts = {}, chdir = true)
466       ENV['GIT_DIR'] = @git_dir if (@git_dir != ENV['GIT_DIR'])
467       ENV['GIT_INDEX_FILE'] = @git_index_file if (@git_index_file != ENV['GIT_INDEX_FILE'])
468       ENV['GIT_WORK_TREE'] = @git_work_dir if (@git_work_dir != ENV['GIT_WORK_TREE'])
469       path = @git_work_dir || @git_dir || @path
471       opts = opts.to_a.join(' ')
472       git_cmd = "git #{cmd} #{opts}"
474       out = nil
475       if chdir && (Dir.getwd != path)
476         Dir.chdir(path) { out = `git #{cmd} #{opts} 2>&1`.chomp } 
477       else
478         out = `git #{cmd} #{opts} 2>&1`.chomp
479       end
480       
481       #puts git_cmd
482       #puts out
483       #puts
484       
485       if $?.exitstatus > 0
486         if $?.exitstatus == 1 && out == ''
487           return ''
488         end
489         raise Git::GitExecuteError.new(git_cmd + ':' + out.to_s) 
490       end
491       out
492     end
493     
494   end