module Grit class Commit extend Lazy attr_reader :id attr_reader :repo lazy_reader :parents lazy_reader :tree lazy_reader :author lazy_reader :authored_date lazy_reader :committer lazy_reader :committed_date lazy_reader :message lazy_reader :short_message # Parses output from the `git-cat-file --batch'. # # repo - Grit::Repo instance. # sha - String SHA of the Commit. # size - Fixnum size of the object. # object - Parsed String output from `git cat-file --batch`. # # Returns an Array of Grit::Commit objects. def self.parse_batch(repo, sha, size, object) info, message = object.split("\n\n", 2) lines = info.split("\n") tree = lines.shift.split(' ', 2).last parents = [] parents << lines.shift[7..-1] while lines.first[0, 6] == 'parent' author, authored_date = Grit::Commit.actor(lines.shift) committer, committed_date = Grit::Commit.actor(lines.shift) Grit::Commit.new( repo, sha, parents, tree, author, authored_date, committer, committed_date, message.to_s.split("\n")) end # Instantiate a new Commit # +id+ is the id of the commit # +parents+ is an array of commit ids (will be converted into Commit instances) # +tree+ is the correspdonding tree id (will be converted into a Tree object) # +author+ is the author string # +authored_date+ is the authored Time # +committer+ is the committer string # +committed_date+ is the committed Time # +message+ is an array of commit message lines # # Returns Grit::Commit (baked) def initialize(repo, id, parents, tree, author, authored_date, committer, committed_date, message) @repo = repo @id = id @parents = parents.map { |p| Commit.create(repo, :id => p) } @tree = Tree.create(repo, :id => tree) @author = author @authored_date = authored_date @committer = committer @committed_date = committed_date @message = message.join("\n") @short_message = message.find { |x| !x.strip.empty? } || '' end def id_abbrev @id_abbrev ||= @repo.git.rev_parse({}, self.id).chomp[0, 7] end # Create an unbaked Commit containing just the specified attributes # +repo+ is the Repo # +atts+ is a Hash of instance variable data # # Returns Grit::Commit (unbaked) def self.create(repo, atts) self.allocate.create_initialize(repo, atts) end # Initializer for Commit.create # +repo+ is the Repo # +atts+ is a Hash of instance variable data # # Returns Grit::Commit (unbaked) def create_initialize(repo, atts) @repo = repo atts.each do |k, v| instance_variable_set("@#{k}", v) end self end def lazy_source self.class.find_all(@repo, @id, {:max_count => 1}).first end # Count the number of commits reachable from this ref # +repo+ is the Repo # +ref+ is the ref from which to begin (SHA1 or name) # # Returns Integer def self.count(repo, ref) repo.git.rev_list({}, ref).size / 41 end # Find all commits matching the given criteria. # +repo+ is the Repo # +ref+ is the ref from which to begin (SHA1 or name) or nil for --all # +options+ is a Hash of optional arguments to git # :max_count is the maximum number of commits to fetch # :skip is the number of commits to skip # # Returns Grit::Commit[] (baked) def self.find_all(repo, ref, options = {}) allowed_options = [:max_count, :skip, :since] default_options = {:pretty => "raw"} actual_options = default_options.merge(options) if ref output = repo.git.rev_list(actual_options, ref) else output = repo.git.rev_list(actual_options.merge(:all => true)) end self.list_from_string(repo, output) rescue Grit::GitRuby::Repository::NoSuchShaFound [] end # Parse out commit information into an array of baked Commit objects # +repo+ is the Repo # +text+ is the text output from the git command (raw format) # # Returns Grit::Commit[] (baked) # # really should re-write this to be more accepting of non-standard commit messages # - it broke when 'encoding' was introduced - not sure what else might show up # def self.list_from_string(repo, text) text_gpgless = text.gsub(/gpgsig -----BEGIN PGP SIGNATURE-----[\n\r](.*[\n\r])*? -----END PGP SIGNATURE-----[\n\r]/, "") lines = text_gpgless.split("\n") commits = [] while !lines.empty? # GITLAB patch # Skip all garbage unless we get real commit while !lines.empty? && lines.first !~ /^commit [a-zA-Z0-9]*$/ lines.shift end id = lines.shift.split.last tree = lines.shift.split.last parents = [] parents << lines.shift.split.last while lines.first =~ /^parent/ author_line = lines.shift author_line << lines.shift if lines[0] !~ /^committer / author, authored_date = self.actor(author_line) committer_line = lines.shift committer_line << lines.shift if lines[0] && lines[0] != '' && lines[0] !~ /^encoding/ committer, committed_date = self.actor(committer_line) # not doing anything with this yet, but it's sometimes there encoding = lines.shift.split.last if lines.first =~ /^encoding/ # GITLAB patch # Skip Signature and other raw data lines.shift while lines.first =~ /^ / lines.shift message_lines = [] message_lines << lines.shift[4..-1] while lines.first =~ /^ {4}/ lines.shift while lines.first && lines.first.empty? commits << Commit.new(repo, id, parents, tree, author, authored_date, committer, committed_date, message_lines) end commits end # Show diffs between two trees. # # repo - The current Grit::Repo instance. # a - A String named commit. # b - An optional String named commit. Passing an array assumes you # wish to omit the second named commit and limit the diff to the # given paths. # paths - An optional Array of paths to limit the diff. # options - An optional Hash of options. Merged into {:full_index => true}. # # Returns Grit::Diff[] (baked) def self.diff(repo, a, b = nil, paths = [], options = {}) if b.is_a?(Array) paths = b b = nil end paths.unshift("--") unless paths.empty? paths.unshift(b) unless b.nil? paths.unshift(a) options = {:full_index => true}.update(options) text = repo.git.diff(options, *paths) Diff.list_from_string(repo, text) end def show if parents.size > 1 diff = @repo.git.native(:diff, {:full_index => true}, "#{parents[0].id}...#{parents[1].id}") else diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id) end if diff =~ /diff --git a/ diff = diff.sub(/.*?(diff --git a)/m, '\1') else diff = '' end Diff.list_from_string(@repo, diff) end # Shows diffs between the commit's parent and the commit. # # options - An optional Hash of options, passed to Grit::Commit.diff. # # Returns Grit::Diff[] (baked) def diffs(options = {}) show end def stats stats = @repo.commit_stats(self.sha, 1)[0][-1] end # Convert this Commit to a String which is just the SHA1 id def to_s @id end def sha @id end def date @committed_date end def to_patch @repo.git.format_patch({'1' => true, :stdout => true}, to_s) end def notes ret = {} notes = Note.find_all(@repo) notes.each do |note| if n = note.commit.tree/(self.id) ret[note.name] = n.data end end ret end # Calculates the commit's Patch ID. The Patch ID is essentially the SHA1 # of the diff that the commit is introducing. # # Returns the 40 character hex String if a patch-id could be calculated # or nil otherwise. def patch_id show = @repo.git.show({}, @id) patch_line = @repo.git.native(:patch_id, :input => show) if patch_line =~ /^([0-9a-f]{40}) [0-9a-f]{40}\n$/ $1 else nil end end # Pretty object inspection def inspect %Q{#} end # private # Parse out the actor (author or committer) info # # Returns [String (actor name and email), Time (acted at time)] def self.actor(line) m, actor, epoch = *line.match(/^.+? (.*) (\d+) .*$/) [Actor.from_string(actor), Time.at(epoch.to_i)] end def author_string "%s <%s> %s %+05d" % [author.name, author.email, authored_date.to_i, 800] end def to_hash { 'id' => id, 'parents' => parents.map { |p| { 'id' => p.id } }, 'tree' => tree.id, 'message' => message, 'author' => { 'name' => author.name, 'email' => author.email }, 'committer' => { 'name' => committer.name, 'email' => committer.email }, 'authored_date' => authored_date.xmlschema, 'committed_date' => committed_date.xmlschema, } end end # Commit end # Grit