diff --git a/TODO b/TODO index 79694fa8..343049e1 100644 --- a/TODO +++ b/TODO @@ -3,7 +3,8 @@ * git revert, rebase * diff additions - - annotate, blame + - annotate + - blame complete, but perhaps needs some more work/addition of missing cli options * submodule support diff --git a/VERSION b/VERSION index c813fe11..3c43790f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.5 +1.2.6 diff --git a/git.gemspec b/git.gemspec index 00e2b9f3..7dfd34e0 100644 --- a/git.gemspec +++ b/git.gemspec @@ -1,15 +1,15 @@ # Generated by jeweler -# DO NOT EDIT THIS FILE -# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec` +# DO NOT EDIT THIS FILE DIRECTLY +# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command # -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = %q{git} - s.version = "1.2.5" + s.version = "1.2.6" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Scott Chacon"] - s.date = %q{2009-10-17} + s.date = %q{2010-09-05} s.email = %q{schacon@gmail.com} s.extra_rdoc_files = [ "README" @@ -18,6 +18,7 @@ Gem::Specification.new do |s| "lib/git.rb", "lib/git/author.rb", "lib/git/base.rb", + "lib/git/blame.rb", "lib/git/branch.rb", "lib/git/branches.rb", "lib/git/diff.rb", @@ -38,7 +39,7 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] s.requirements = ["git 1.6.0.0, or greater"] s.rubyforge_project = %q{git} - s.rubygems_version = %q{1.3.5} + s.rubygems_version = %q{1.3.6} s.summary = %q{Ruby/Git is a Ruby library that can be used to create, read and manipulate Git repositories by wrapping system calls to the git binary} if s.respond_to? :specification_version then @@ -51,3 +52,4 @@ Gem::Specification.new do |s| else end end + diff --git a/lib/git.rb b/lib/git.rb index acf7d1b3..5041c7d1 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -21,6 +21,7 @@ require 'git/diff' require 'git/status' +require 'git/blame' require 'git/author' require 'git/stashes' diff --git a/lib/git/base.rb b/lib/git/base.rb index 5ad8906a..7072b769 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -174,6 +174,11 @@ def log(count = 30) def status Git::Status.new(self) end + + # return a Git::Blame object + def blame(file = '', opts = {}) + Git::Blame.new(self, file, opts) + end # returns a Git::Branches object of all the Git::Branch objects for this repo def branches diff --git a/lib/git/blame.rb b/lib/git/blame.rb new file mode 100644 index 00000000..3e81c014 --- /dev/null +++ b/lib/git/blame.rb @@ -0,0 +1,153 @@ +module Git + + class Blame + include Enumerable + + attr_reader :lines + + def initialize(base, file = '', opts = {}) + @base = base + + construct_blame(file, opts) + end + + # Todo - factor this in to BlameLine instead, and have this loop? + def pretty + out = '' + + self.each do |line| + out << line.line + ' : ' + line.commit + "\n" + out << ' ' + line.summary + "\n" + out << " author:\n" + out << ' ' + line.author + "\n" + out << ' ' + line.author_email + "\n" + out << ' @ ' + line.author_timestamp + "\n" + out << ' ' + line.author_timezone + "\n" + out << "\n" + out << " committer:\n" + out << ' ' + line.committer + "\n" + out << ' ' + line.committer_email + "\n" + out << ' @ ' + line.committer_timestamp + "\n" + out << ' ' + line.committer_timezone + "\n" + out << "\n" + end + + out + end + + + def [](line) + @lines[line] + end + + # enumerable method + + def each(&block) + @lines.values.each(&block) + end + + + class BlameLine + attr_accessor :line, :commit + attr_accessor :author, :author_email, :author_timestamp, :author_timezone + attr_accessor :committer, :committer_email, :committer_timestamp, :committer_timezone + attr_accessor :summary + + + def initialize(line, hash) + @line = line + + @commit = hash[:commit] + + @author = hash[:author] + @author_email = hash[:author_email] + @author_timestamp = hash[:author_timestamp] + @author_timezone = hash[:author_timezone] + + @committer = hash[:committer] + @committer_email = hash[:committer_email] + @committer_timestamp = hash[:committer_timestamp] + @committer_timezone = hash[:committer_timezone] + + @summary = hash[:summary] + end + + end + + + private + + @lines + + + # This will run the blame (via our lib.rb), and parse the porcelain-formatted blame output into BlameLine objects + def construct_blame(file = '', opts = {}) + @lines = {} + + opts[:file] = file + + lines = @base.lib.blame(opts) + + parsed_lines = {} + commits = {} + + commit = nil + + lines.each do |line| + new_commit = line.match(/^[a-fA-F0-9]{40}/) + + if ! new_commit.nil? + commit = new_commit[0] + + line_num = line.sub(/^[a-f0-9]{40} [0-9]+ /, '') + + if line_num.match(/\s[0-9]+/) + block_length = line_num.sub(/^[0-9]+\s/, '').sub(/\s.*$/, '') + + line_num = line_num.sub(/\s[0-9]+.*$/, '') + else + block_length = 1 + end + + # this looks odd, but it's correct... we're initializing this commit's hash, which + # should contain a :hash -> element, among other things, and the hash + # OF commit hashes is indexed on the sha hash, so... yeah :) + # + commits[commit] = {:commit => commit} if ! commits[commit] + + for i in line_num.to_i..(line_num.to_i + block_length.to_i - 1) + parsed_lines[i] = commit + end + end + + if /^author\s/.match(line) + commits[commit][:author] = line.sub(/^author\s/, '') + elsif /^author-mail\s/.match(line) + commits[commit][:author_email] = line.sub(/^author-mail\s/, '') + elsif /^author-time\s/.match(line) + commits[commit][:author_timestamp] = line.sub(/^author-time\s/, '') + elsif /^author-tz\s/.match(line) + commits[commit][:author_timezone] = line.sub(/^author-tz\s/, '') + elsif /^committer\s/.match(line) + commits[commit][:committer] = line.sub(/^committer\s/, '') + elsif /^committer-mail\s/.match(line) + commits[commit][:committer_email] = line.sub(/^committer-mail\s/, '') + elsif /^committer-time\s/.match(line) + commits[commit][:committer_timestamp] = line.sub(/^committer-time\s/, '') + elsif /^committer-tz\s/.match(line) + commits[commit][:committer_timezone] = line.sub(/^committer-tz\s/, '') + elsif /^summary\s/.match(line) + commits[commit][:summary] = line.sub(/^summary\s/, '') + end + end + + parsed_lines.each do |line, commit| + commits[commit][:line] = line + + @lines[line] = BlameLine.new(line, commits[commit]) + end + end + + end + +end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 52fb2e6c..69074341 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -92,6 +92,30 @@ def full_log_commits(opts = {}) full_log = command_lines('log', arr_opts, true) process_commit_data(full_log) end + + def blame(opts = {}) + if '' == opts[:file] + raise "Can't blame a null filename!" + end + + arr_opts = [] + + arr_opts << '-b' if opts[:blank_boundaries] + arr_opts << '--root' if opts[:root] + arr_opts << '-t' if opts[:raw_timestamps] + arr_opts << '-p' + arr_opts << '-w' if opts[:ignore_whitespace] + arr_opts << '--incremental' + arr_opts << "-S #{opts[:rev_file]}" if (opts[:rev_file] && opts[:rev_file].is_a?(String)) + arr_opts << '-L' && arr_opts << "#{opts[:start]},#{opts[:fin]}" + arr_opts << "--since=#{opts[:since]}" if (opts[:since] && opts[:since].is_a?(String)) + + arr_opts << opts[:rev] if (opts[:rev] && opts[:rev].is_a?(String)) + + arr_opts << opts[:file] + + command_lines('blame', arr_opts, true) + end def revparse(string) return string if string =~ /[A-Fa-f0-9]{40}/ # passing in a sha - just no-op it