diff --git a/lib/git.rb b/lib/git.rb index 9ef0fc09..e2c27005 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -11,6 +11,7 @@ require 'git/index' require 'git/lib' require 'git/log' +require 'git/blame' require 'git/object' require 'git/path' require 'git/remote' diff --git a/lib/git/base.rb b/lib/git/base.rb index dd9b6bb7..40355fe7 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -148,7 +148,10 @@ def config(name = nil, value = nil) # on the objectish and determine the type of the object and return # an appropriate object for that type def object(objectish) + return nil unless objectish Git::Object.new(self, objectish) + rescue Git::GitExecuteError + return nil # unknown revision end def gtree(objectish) @@ -168,6 +171,12 @@ def log(count = 30) Git::Log.new(self, count) end + # returns a Git::BlameResult object with the commits that most recently + # modified each line + def blame(file, opts={}) + Git::Blame.new(self, file, opts).result + end + # returns a Git::Status object def status Git::Status.new(self) diff --git a/lib/git/blame.rb b/lib/git/blame.rb new file mode 100644 index 00000000..fb36f151 --- /dev/null +++ b/lib/git/blame.rb @@ -0,0 +1,48 @@ +class Git::Blame + attr_reader :result + + def initialize(base, file, opts={}) + @base = base + @file = file + @options = opts + + @result = run_blame + end + + private + + def run_blame + lines = @base.lib.blame(@file, @options) + result = Git::BlameResult.new(@base) + lines.each do |line| + words = line.split(/\s+/) + next unless words.first.length == 40 #TODO this breaks if git ever introduces a 40-character attribute name + ref = words[0] + block_start = words[2].to_i + block_length = words[3].to_i + result.add_block ref, block_start, block_length + end + result + end +end + +class Git::BlameResult + def [](*args) lines[*args] end + def each(*args, &block) lines.each(*args, &block) end + def each_with_index(*args, &block) lines.each_with_index(*args, &block) end + + def initialize(base) + @base = base + @commit_cache = {} + @lines = [] + end + + def add_block(ref, start, length) + commit = (@commit_cache[ref] ||= @base.object(ref)) + length.times { |i| @lines[start + i] = commit } + commit + end + + attr_reader :lines + private :lines +end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 21de727a..3eb1e250 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -26,7 +26,7 @@ def initialize(base = nil, logger = nil) end def init - command('init') + command('init').chomp end # tries to clone the given repo @@ -48,6 +48,7 @@ def clone(repository, name, opts = {}) arr_opts = [] arr_opts << "--bare" if opts[:bare] + arr_opts << "--mirror" if opts[:mirror] arr_opts << "--recursive" if opts[:recursive] arr_opts << "-o" << opts[:remote] if opts[:remote] arr_opts << "--depth" << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0 @@ -57,9 +58,9 @@ def clone(repository, name, opts = {}) arr_opts << repository arr_opts << clone_dir - command('clone', arr_opts) + command('clone', arr_opts).chomp - opts[:bare] ? {:repository => clone_dir} : {:working_directory => clone_dir} + (opts[:bare] || opts[:mirror]) ? {:repository => clone_dir} : {:working_directory => clone_dir} end @@ -69,6 +70,7 @@ def log_commits(opts={}) arr_opts = log_common_options(opts) arr_opts << '--pretty=oneline' + arr_opts << '--no-color' arr_opts += log_path_options(opts) @@ -79,6 +81,7 @@ def full_log_commits(opts={}) arr_opts = log_common_options(opts) arr_opts << '--pretty=raw' + arr_opts << '--no-color' arr_opts << "--skip=#{opts[:skip]}" if opts[:skip] arr_opts += log_path_options(opts) @@ -87,7 +90,30 @@ def full_log_commits(opts={}) process_commit_data(full_log) end + def blame(file, opts={}) + arr_opts = %w( --incremental ) + arr_opts << "-L#{opts[:start]},#{opts[:end]}" if opts[:start] && opts[:end] + arr_opts << '--reverse' if opts[:reverse] + case opts[:detect_intrafile_moves] + when true then arr_opts << '-M' + when Fixnum then arr_opts << "-M#{opts[:detect_intrafile_moves]}" + end + if opts[:detect_interfile_moves] + c_opt = if opts[:check_all_commits_for_interfile_moves] + '-CCC' + elsif opts[:check_first_file_commit_for_interfile_moves] + '-CC' + else + '-C' + end + c_opt << opts[:detect_interfile_moves] if opts[:detect_interfile_moves].kind_of?(Fixnum) + end + arr_opts << '-w' if opts[:ignore_whitespace] + arr_opts << opts[:revision] if opts[:revision] + arr_opts << '--' << file + command_lines 'blame', arr_opts + end def revparse(string) return string if string =~ /[A-Fa-f0-9]{40}/ # passing in a sha - just no-op it @@ -97,15 +123,15 @@ def revparse(string) File.file?(path) end return File.read(rev).chomp if rev - command('rev-parse', string) + command('rev-parse', string).chomp end def namerev(string) - command('name-rev', string).split[1] + command('name-rev', string).split[1].chomp end def object_type(sha) - command('cat-file', ['-t', sha]) + command('cat-file', ['-t', sha]).chomp end def object_size(sha) @@ -164,7 +190,7 @@ def object_contents(sha, &block) end def ls_tree(sha) - data = {'blob' => {}, 'tree' => {}} + data = {'blob' => {}, 'tree' => {}, 'commit' => {}, 'tag' => {}} command_lines('ls-tree', sha).each do |line| (info, filenm) = line.split("\t") @@ -188,7 +214,7 @@ def tree_depth(sha) end def change_head_branch(branch_name) - command('symbolic-ref', ['HEAD', "refs/heads/#{branch_name}"]) + command('symbolic-ref', ['HEAD', "refs/heads/#{branch_name}"]).chomp end def branches_all @@ -242,7 +268,7 @@ def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) diff_opts << obj2 if obj2.is_a?(String) diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String - command('diff', diff_opts) + command('diff', diff_opts).chomp end def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) @@ -304,7 +330,7 @@ def config_remote(name) def config_get(name) do_get = lambda do |path| - command('config', ['--get', name]) + command('config', ['--get', name]).chomp end if @git_dir @@ -315,7 +341,7 @@ def config_get(name) end def global_config_get(name) - command('config', ['--global', '--get', name], false) + command('config', ['--global', '--get', name], false).chomp end def config_list @@ -350,11 +376,11 @@ def parse_config(file) ## WRITE COMMANDS ## def config_set(name, value) - command('config', [name, value]) + command('config', [name, value]).chomp end def global_config_set(name, value) - command('config', ['--global', name, value], false) + command('config', ['--global', name, value], false).chomp end # updates the repository index using the workig dorectory content @@ -379,7 +405,7 @@ def add(paths='.',options={}) arr_opts.flatten! - command('add', arr_opts) + command('add', arr_opts).chomp end def remove(path = '.', opts = {}) @@ -392,7 +418,7 @@ def remove(path = '.', opts = {}) arr_opts << path end - command('rm', arr_opts) + command('rm', arr_opts).chomp end def commit(message, opts = {}) @@ -400,26 +426,26 @@ def commit(message, opts = {}) arr_opts << '-a' if opts[:add_all] arr_opts << '--allow-empty' if opts[:allow_empty] arr_opts << "--author" << opts[:author] if opts[:author] - command('commit', arr_opts) + command('commit', arr_opts).chomp end def reset(commit, opts = {}) arr_opts = [] arr_opts << '--hard' if opts[:hard] arr_opts << commit if commit - command('reset', arr_opts) + command('reset', arr_opts).chomp end def apply(patch_file) arr_opts = [] arr_opts << '--' << patch_file if patch_file - command('apply', arr_opts) + command('apply', arr_opts).chomp end def apply_mail(patch_file) arr_opts = [] arr_opts << '--' << patch_file if patch_file - command('am', arr_opts) + command('am', arr_opts).chomp end def stashes_all @@ -435,32 +461,32 @@ def stashes_all end def stash_save(message) - output = command('stash save', ['--', message]) + output = command('stash save', ['--', message]).chomp output =~ /HEAD is now at/ end def stash_apply(id = nil) if id - command('stash apply', [id]) + command('stash apply', [id]).chomp else - command('stash apply') + command('stash apply').chomp end end def stash_clear - command('stash clear') + command('stash clear').chomp end def stash_list - command('stash list') + command('stash list').chomp end def branch_new(branch) - command('branch', branch) + command('branch', branch).chomp end def branch_delete(branch) - command('branch', ['-D', branch]) + command('branch', ['-D', branch]).chomp end def checkout(branch, opts = {}) @@ -469,21 +495,21 @@ def checkout(branch, opts = {}) arr_opts << '-b' << opts[:new_branch] if opts[:new_branch] arr_opts << branch - command('checkout', arr_opts) + command('checkout', arr_opts).chomp end def checkout_file(version, file) arr_opts = [] arr_opts << version arr_opts << file - command('checkout', arr_opts) + command('checkout', arr_opts).chomp end def merge(branch, message = nil) arr_opts = [] arr_opts << '-m' << message if message arr_opts += [branch] - command('merge', arr_opts) + command('merge', arr_opts).chomp end def unmerged @@ -497,10 +523,10 @@ def unmerged def conflicts # :yields: file, your, their self.unmerged.each do |f| your = Tempfile.new("YOUR-#{File.basename(f)}").path - command('show', ":2:#{f}", true, "> #{escape your}") + command('show', ":2:#{f}", true, "> #{escape your}").chomp their = Tempfile.new("THEIR-#{File.basename(f)}").path - command('show', ":3:#{f}", true, "> #{escape their}") + command('show', ":3:#{f}", true, "> #{escape their}").chomp yield(f, your, their) end end @@ -513,11 +539,11 @@ def remote_add(name, url, opts = {}) arr_opts << name arr_opts << url - command('remote', arr_opts) + command('remote', arr_opts).chomp end def remote_remove(name) - command('remote', ['rm', name]) + command('remote', ['rm', name]).chomp end def remotes @@ -529,32 +555,32 @@ def tags end def tag(tag) - command('tag', tag) + command('tag', tag).chomp end def fetch(remote) - command('fetch', remote) + command('fetch', remote).chomp end def push(remote, branch = 'master', tags = false) - command('push', [remote, branch]) - command('push', ['--tags', remote]) if tags + command('push', [remote, branch]).chomp + command('push', ['--tags', remote]).chomp if tags end def tag_sha(tag_name) head = File.join(@git_dir, 'refs', 'tags', tag_name) return File.read(head).chomp if File.exists?(head) - command('show-ref', ['--tags', '-s', tag_name]) + command('show-ref', ['--tags', '-s', tag_name]).chomp end def repack - command('repack', ['-a', '-d']) + command('repack', ['-a', '-d']).chomp end def gc - command('gc', ['--prune', '--aggressive', '--auto']) + command('gc', ['--prune', '--aggressive', '--auto']).chomp end # reads a tree into the current index file @@ -562,11 +588,11 @@ def read_tree(treeish, opts = {}) arr_opts = [] arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix] arr_opts += [treeish] - command('read-tree', arr_opts) + command('read-tree', arr_opts).chomp end def write_tree - command('write-tree') + command('write-tree').chomp end def commit_tree(tree, opts = {}) @@ -579,11 +605,11 @@ def commit_tree(tree, opts = {}) arr_opts << tree arr_opts << '-p' << opts[:parent] if opts[:parent] arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents] - command('commit-tree', arr_opts, true, "< #{escape t.path}") + command('commit-tree', arr_opts, true, "< #{escape t.path}").chomp end def update_ref(branch, commit) - command('update-ref', [branch, commit]) + command('update-ref', [branch, commit]).chomp end def checkout_index(opts = {}) @@ -593,7 +619,7 @@ def checkout_index(opts = {}) arr_opts << "--all" if opts[:all] arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String - command('checkout-index', arr_opts) + command('checkout-index', arr_opts).chomp end # creates an archive file @@ -619,13 +645,13 @@ def archive(sha, file = nil, opts = {}) arr_opts << "--remote=#{opts[:remote]}" if opts[:remote] arr_opts << sha arr_opts << '--' << opts[:path] if opts[:path] - command('archive', arr_opts, true, (opts[:add_gzip] ? '| gzip' : '') + " > #{escape file}") + command('archive', arr_opts, true, (opts[:add_gzip] ? '| gzip' : '') + " > #{escape file}").chomp return file end # returns the current version of git, as an Array of Fixnums. def current_command_version - output = command('version', [], false) + output = command('version', [], false).chomp version = output[/\d+\.\d+(\.\d+)+/] version.split('.').collect {|i| i.to_i} end @@ -648,7 +674,7 @@ def meets_required_version? private def command_lines(cmd, opts = [], chdir = true, redirect = '') - command(cmd, opts, chdir).split("\n") + command(cmd, opts, chdir).chomp.split("\n") end def command(cmd, opts = [], chdir = true, redirect = '', &block) @@ -739,7 +765,7 @@ def run_command(git_cmd, &block) if block_given? IO.popen(git_cmd, &block) else - `#{git_cmd}`.chomp + `#{git_cmd}` end end diff --git a/tests/units/test_archive.rb b/tests/units/test_archive.rb old mode 100644 new mode 100755 index efff94a3..0994389b --- a/tests/units/test_archive.rb +++ b/tests/units/test_archive.rb @@ -26,9 +26,9 @@ def test_archive f = @git.object('v2.6').archive(nil, :format => 'tar') # returns path to temp file assert(File.exists?(f)) - lines = `cd /tmp; tar xvpf #{f}`.split("\n") - assert_equal('ex_dir/', lines[0]) - assert_equal('example.txt', lines[2]) + lines = `cd /tmp; tar xvpf #{f} 2>&1`.split("\n") + assert_equal('x ex_dir/', lines[0]) + assert_equal('x example.txt', lines[2]) f = @git.object('v2.6').archive(tempfile, :format => 'zip') assert(File.file?(f)) @@ -39,9 +39,9 @@ def test_archive f = @git.object('v2.6').archive(tempfile, :format => 'tar', :prefix => 'test/', :path => 'ex_dir/') assert(File.exists?(f)) - lines = `cd /tmp; tar xvpf #{f}`.split("\n") - assert_equal('test/', lines[0]) - assert_equal('test/ex_dir/ex.txt', lines[2]) + lines = `cd /tmp; tar xvpf #{f} 2>&1`.split("\n") + assert_equal('x test/', lines[0]) + assert_equal('x test/ex_dir/ex.txt', lines[2]) in_temp_dir do c = Git.clone(@wbare, 'new') diff --git a/tests/units/test_index_ops.rb b/tests/units/test_index_ops.rb old mode 100644 new mode 100755 index dcd84984..27115b40 --- a/tests/units/test_index_ops.rb +++ b/tests/units/test_index_ops.rb @@ -33,7 +33,7 @@ def test_add assert(!g.status.changed.assoc('example.txt')) assert(!g.status.added.assoc('test-file')) assert(!g.status.untracked.assoc('test-file')) - assert_equal('hahahaha', g.status['example.txt'].blob.contents) + assert_equal("hahahaha\n", g.status['example.txt'].blob.contents) end end end @@ -55,7 +55,7 @@ def test_add_array g.commit('my message') assert(!g.status.added.assoc('test-file1')) assert(!g.status.untracked.assoc('test-file1')) - assert_equal('blahblahblah1', g.status['test-file1'].blob.contents) + assert_equal("blahblahblah1\n", g.status['test-file1'].blob.contents) end end end diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb old mode 100644 new mode 100755 index 93ea34f3..c52b63df --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -74,14 +74,14 @@ def test_object_contents commit << "parent 546bec6f8872efa41d5d97a369f669165ecda0de\n" commit << "author scott Chacon 1194561188 -0800\n" commit << "committer scott Chacon 1194561188 -0800\n" - commit << "\ntest" + commit << "\ntest\n" assert_equal(commit, @lib.object_contents('1cc8667014381')) # commit tree = "040000 tree 6b790ddc5eab30f18cabdd0513e8f8dac0d2d3ed\tex_dir\n" - tree << "100644 blob 3aac4b445017a8fc07502670ec2dbf744213dd48\texample.txt" + tree << "100644 blob 3aac4b445017a8fc07502670ec2dbf744213dd48\texample.txt\n" assert_equal(tree, @lib.object_contents('1cc8667014381^{tree}')) #tree - blob = "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n2" + blob = "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n2\n" assert_equal(blob, @lib.object_contents('v2.5:example.txt')) #blob end diff --git a/tests/units/test_object.rb b/tests/units/test_object.rb old mode 100644 new mode 100755 index 870d37f2..13f41a89 --- a/tests/units/test_object.rb +++ b/tests/units/test_object.rb @@ -100,8 +100,8 @@ def test_blob def test_blob_contents o = @git.gblob('v2.6:example.txt') - assert_equal('replace with new text', o.contents) - assert_equal('replace with new text', o.contents) # this should be cached + assert_equal("replace with new text\n", o.contents) + assert_equal("replace with new text\n", o.contents) # this should be cached # make sure the block is called block_called = false