diff --git a/.gitignore b/.gitignore index 4ebc8aea50e0a67e000ba29a30809d0a7b9b2666..2dd02534615434d88c51307beb0f0092f21fd103 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ coverage +pkg diff --git a/Manifest.txt b/Manifest.txt index 641972d82c6d1b51122274ae8f6a0ecdfb56ee22..38bf80c54a526e76d74820a0f48606fe1ca7b1be 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -4,4 +4,31 @@ README.txt Rakefile bin/grit lib/grit.rb -test/test_grit.rb \ No newline at end of file +lib/grit/actor.rb +lib/grit/blob.rb +lib/grit/commit.rb +lib/grit/errors.rb +lib/grit/git.rb +lib/grit/head.rb +lib/grit/lazy.rb +lib/grit/repo.rb +lib/grit/tree.rb +test/fixtures/blame +test/fixtures/cat_file_blob +test/fixtures/cat_file_blob_size +test/fixtures/for_each_ref +test/fixtures/ls_tree_a +test/fixtures/ls_tree_b +test/fixtures/rev_list +test/fixtures/rev_list_single +test/helper.rb +test/profile.rb +test/suite.rb +test/test_actor.rb +test/test_blob.rb +test/test_commit.rb +test/test_git.rb +test/test_head.rb +test/test_reality.rb +test/test_repo.rb +test/test_tree.rb diff --git a/README.txt b/README.txt index 8b1e02c0fb554eed2ce2ef737a68bb369d7527df..fca94f84afd7d749c62626011f972a509f6a5ac6 100644 --- a/README.txt +++ b/README.txt @@ -1,32 +1,185 @@ grit - by FIX (your name) - FIX (url) + by Tom Preston-Werner + grit.rubyforge.org == DESCRIPTION: + +Grit is a Ruby library for extracting information from a git repository in and +object oriented manner. + +== REQUIREMENTS: + +* git (http://git.or.cz) tested with 1.5.3.4 + +== INSTALL: + +sudo gem install grit + +== USAGE: + +Grit gives you object model access to your git repository. Once you have +created a repository object, you can traverse it to find parent commit(s), +trees, blobs, etc. + += Initialize a Repo object + +The first step is to create a GitPython.Repo object to represent your repo. I +include the Grit module so reduce typing. + + include Grit + repo = Repo.new("/Users/tom/dev/grit") -FIX (describe your package) +In the above example, the directory /Users/tom/dev/grit is my working +repo and contains the .git directory. You can also initialize Grit with a +bare repo. -== FEATURES/PROBLEMS: + repo = Repo.new("/var/git/grit.git") -* FIX (list of features or problems) += Getting a list of commits -== SYNOPSIS: +From the Repo object, you can get a list of commits as an array of Commit +objects. - FIX (code sample of usage) + repo.commits + # => [#, + #, + #, + #, + #] + +Called without arguments, Repo#commits returns a list of up to ten commits +reachable by the master branch (starting at the latest commit). You can ask +for commits beginning at a different branch, commit, tag, etc. -== REQUIREMENTS: + repo.commits('mybranch') + repo.commits('40d3057d09a7a4d61059bca9dca5ae698de58cbe') + repo.commits('v0.1') + +You can specify the maximum number of commits to return. -* FIX (list of requirements) + repo.commits('master', 100) + +If you need paging, you can specify a number of commits to skip. -== INSTALL: + repo.commits('master', 10, 20) + +The above will return commits 21-30 from the commit list. + += The Commit object + +Commit objects contain information about that commit. + + head = repo.commits.first + + head.id + # => "e80bbd2ce67651aa18e57fb0b43618ad4baf7750" + + head.parents + # => [#] + + head.tree + # => # + + head.author + # => #"> + + head.authored_date + # => Wed Oct 24 22:02:31 -0700 2007 + + head.committer + # => #"> + + head.committed_date + # => Wed Oct 24 22:02:31 -0700 2007 + + head.message + # => "add Actor inspect" + +You can traverse a commit's ancestry by chaining calls to #parents. + + repo.commits.first.parents[0].parents[0].parents[0] + +The above corresponds to master^^^ or master~3 in git parlance. + += The Tree object + +A tree records pointers to the contents of a directory. Let's say you want +the root tree of the latest commit on the master branch. + + tree = repo.commits.first.tree + # => # + + tree.id + # => "3536eb9abac69c3e4db583ad38f3d30f8db4771f" + +Once you have a tree, you can get the contents. + + contents = tree.contents + # => [#, + #, + #, + #] + +This tree contains two Blob objects and two Tree objects. The trees are +subdirectories and the blobs are files. Trees below the root have additional +attributes. + + contents.last.name + # => "lib" + + contents.last.mode + # => "040000" + +There is a convenience method that allows you to get a named sub-object +from a tree. + + tree/"lib" + # => # + +You can also get a tree directly from the repo if you know its name. + + repo.tree + # => # + + repo.tree("91169e1f5fa4de2eaea3f176461f5dc784796769") + # => # + += The Blob object + +A blob represents a file. Trees often contain blobs. + + blob = tree.contents.first + # => # + +A blob has certain attributes. + + blob.id + # => "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666" + + blob.name + # => "README.txt" + + blob.mode + # => "100644" + + blob.size + # => 7726 + +You can get the data of a blob as a string. + + blob.data + # => "Grit is a library to ..." + +You can also get a blob directly from the repo if you know its name. -* FIX (sudo gem install, anything else) + repo.blob("4ebc8aea50e0a67e000ba29a30809d0a7b9b2666") + # => # == LICENSE: (The MIT License) -Copyright (c) 2007 FIX +Copyright (c) 2007 Tom Preston-Werner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/Rakefile b/Rakefile index 5bfb62163af455ca54422fd0b2e723ba1021ad12..72fde8c9ca87a1c992ce992bab13c3c4f13cddb9 100644 --- a/Rakefile +++ b/Rakefile @@ -4,11 +4,11 @@ require './lib/grit.rb' Hoe.new('grit', GitPython.VERSION) do |p| p.rubyforge_name = 'grit' - # p.author = 'FIX' - # p.email = 'FIX' - # p.summary = 'FIX' - # p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n") - # p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1] + p.author = 'Tom Preston-Werner' + p.email = 'tom@rubyisawesome.com' + p.summary = 'Object model interface to a git repo' + p.description = p.paragraphs_of('README.txt', 2..2).join("\n\n") + p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[2..-1].map { |u| u.strip } p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n") end diff --git a/lib/grit.rb b/lib/grit.rb index ae0792ae39d4891ebc1af996102a4f9df703394d..ae55fd7961ac49233f6ca515622a61e90d516044 100644 --- a/lib/grit.rb +++ b/lib/grit.rb @@ -1,4 +1,4 @@ -$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed +$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed # core @@ -12,6 +12,8 @@ require 'grit/head' require 'grit/commit' require 'grit/tree' require 'grit/blob' +require 'grit/actor' +require 'grit/diff' require 'grit/repo' module Grit @@ -21,5 +23,5 @@ module Grit self.debug = false - VERSION = '1.0.0' + VERSION = '0.1.0' end \ No newline at end of file diff --git a/lib/grit/actor.rb b/lib/grit/actor.rb new file mode 100644 index 0000000000000000000000000000000000000000..f733bce6b57c0e5e353206e692b0e3105c2527f4 --- /dev/null +++ b/lib/grit/actor.rb @@ -0,0 +1,35 @@ +module Grit + + class Actor + attr_reader :name + attr_reader :email + + def initialize(name, email) + @name = name + @email = email + end + + # Create an Actor from a string. + # +str+ is the string, which is expected to be in regular git format + # + # Format + # John Doe + # + # Returns Actor + def self.from_string(str) + case str + when /<.+>/ + m, name, email = *str.match(/(.*) <(.+?)>/) + return self.new(name, email) + else + return self.new(str, nil) + end + end + + # Pretty object inspection + def inspect + %Q{#">} + end + end # Actor + +end # Grit \ No newline at end of file diff --git a/lib/grit/blob.rb b/lib/grit/blob.rb index c863646d4278bfee2a7bcb64caace6b31f89ef03..87d43fab37844afdc2f8814dba3abdaa791f1370 100644 --- a/lib/grit/blob.rb +++ b/lib/grit/blob.rb @@ -81,9 +81,9 @@ module Grit c = commits[info[:id]] unless c c = Commit.create(repo, :id => info[:id], - :author => info[:author], + :author => Actor.from_string(info[:author] + ' ' + info[:author_email]), :authored_date => info[:author_date], - :committer => info[:committer], + :committer => Actor.from_string(info[:committer] + ' ' + info[:committer_email]), :committed_date => info[:committer_date], :message => info[:summary]) commits[info[:id]] = c @@ -102,11 +102,6 @@ module Grit def inspect %Q{#} end - - # private - - def self.read_ - end end # Blob end # Grit \ No newline at end of file diff --git a/lib/grit/commit.rb b/lib/grit/commit.rb index c2a9e2f81657b19925fe9bab4bc5d7ac130e5880..cd9c3e3184c97e83a8982fab9499cad3aec339f6 100644 --- a/lib/grit/commit.rb +++ b/lib/grit/commit.rb @@ -136,6 +136,11 @@ module Grit commits end + def self.diff(repo, id) + text = repo.git.diff({:full_index => true}, id) + Diff.list_from_string(repo, text) + end + # Convert this Commit to a String which is just the SHA1 id def to_s @id @@ -153,7 +158,7 @@ module Grit # Returns [String (actor name and email), Time (acted at time)] def self.actor(line) m, actor, epoch = *line.match(/^.+? (.*) (\d+) .*$/) - [actor, Time.at(epoch.to_i)] + [Actor.from_string(actor), Time.at(epoch.to_i)] end end # Commit diff --git a/lib/grit/git.rb b/lib/grit/git.rb index 1d5251d40fb65ac89184ec662a3e1b04d0c24861..98eeddda5ed2b0e215e21128112393bdc9bc9039 100644 --- a/lib/grit/git.rb +++ b/lib/grit/git.rb @@ -13,17 +13,6 @@ module Grit self.git_dir = git_dir end - # Converstion hash from Ruby style options to git command line - # style options - TRANSFORM = {:max_count => "--max-count=", - :skip => "--skip=", - :pretty => "--pretty=", - :sort => "--sort=", - :format => "--format=", - :since => "--since=", - :p => "-p", - :s => "-s"} - # Run the given git command with the specified arguments and return # the result as a String # +cmd+ is the command @@ -52,12 +41,19 @@ module Grit def transform_options(options) args = [] options.keys.each do |opt| - if TRANSFORM[opt] + if opt.to_s.size == 1 + if options[opt] == true + args << "-#{opt}" + else + val = options.delete(opt) + args << "-#{opt.to_s} #{val}" + end + else if options[opt] == true - args << TRANSFORM[opt] + args << "--#{opt.to_s.gsub(/_/, '-')}" else val = options.delete(opt) - args << TRANSFORM[opt] + val.to_s + args << "--#{opt.to_s.gsub(/_/, '-')}=#{val}" end end end diff --git a/lib/grit/repo.rb b/lib/grit/repo.rb index 624991d07e240ae66ff2a0dc55e2f2b5e262c75b..63bf03b839374c96a3d42a07d56681a797f52a71 100644 --- a/lib/grit/repo.rb +++ b/lib/grit/repo.rb @@ -93,6 +93,17 @@ module Grit def blob(id) Blob.create(self, :id => id) end + + # The commit log for a treeish + # + # Returns GitPython.Commit[] + def log(commit = 'master', path = nil, options = {}) + default_options = {:pretty => "raw"} + actual_options = default_options.merge(options) + arg = path ? "#{commit} -- #{path}" : commit + commits = self.git.log(actual_options, arg) + Commit.list_from_string(self, commits) + end # The diff from commit +a+ to commit +b+, optionally restricted to the given file(s) # +a+ is the base commit @@ -121,4 +132,4 @@ module Grit end end # Repo -end # Grit \ No newline at end of file +end # Grit diff --git a/test/test_actor.rb b/test/test_actor.rb new file mode 100644 index 0000000000000000000000000000000000000000..08391f12336831d048122c8d13bc8404f27e6b91 --- /dev/null +++ b/test/test_actor.rb @@ -0,0 +1,28 @@ +require File.dirname(__FILE__) + '/helper' + +class TestActor < Test::Unit::TestCase + def setup + + end + + # from_string + + def test_from_string_should_separate_name_and_email + a = Actor.from_string("Tom Werner ") + assert_equal "Tom Werner", a.name + assert_equal "tom@example.com", a.email + end + + def test_from_string_should_handle_just_name + a = Actor.from_string("Tom Werner") + assert_equal "Tom Werner", a.name + assert_equal nil, a.email + end + + # inspect + + def test_inspect + a = Actor.from_string("Tom Werner ") + assert_equal %Q{#">}, a.inspect + end +end \ No newline at end of file diff --git a/test/test_blob.rb b/test/test_blob.rb index 6fa087d785661843034d03c7e0b917a8a80d5d8c..9ef84cc14266141b070771706b8aeebc3dfbef82 100644 --- a/test/test_blob.rb +++ b/test/test_blob.rb @@ -40,9 +40,11 @@ class TestBlob < Test::Unit::TestCase c = b.first.first c.expects(:__bake__).times(0) assert_equal '634396b2f541a9f2d58b00be1a07f0c358b999b3', c.id - assert_equal 'Tom Preston-Werner', c.author + assert_equal 'Tom Preston-Werner', c.author.name + assert_equal 'tom@mojombo.com', c.author.email assert_equal Time.at(1191997100), c.authored_date - assert_equal 'Tom Preston-Werner', c.committer + assert_equal 'Tom Preston-Werner', c.committer.name + assert_equal 'tom@mojombo.com', c.committer.email assert_equal Time.at(1191997100), c.committed_date assert_equal 'initial grit setup', c.message # c.expects(:__bake__).times(1) diff --git a/test/test_commit.rb b/test/test_commit.rb index 3bd6af75deda05725900eb7fd06e8107df14c655..0936c90e5b29ede2b5214d6dc26d256a8c6646f4 100644 --- a/test/test_commit.rb +++ b/test/test_commit.rb @@ -10,9 +10,28 @@ class TestCommit < Test::Unit::TestCase def test_bake Git.any_instance.expects(:rev_list).returns(fixture('rev_list_single')) @c = Commit.create(@r, :id => '4c8124ffcf4039d292442eeccabdeca5af5c5017') - @c.author # cause bake-age + @c.author # bake - assert_equal "Tom Preston-Werner ", @c.author + assert_equal "Tom Preston-Werner", @c.author.name + assert_equal "tom@mojombo.com", @c.author.email + end + + # diff + + def test_diff + Git.any_instance.expects(:diff).returns(fixture('diff_p')) + diffs = Commit.diff(@r, 'master') + + assert_equal 15, diffs.size + + assert_equal '.gitignore', diffs.first.a_path + assert_equal '.gitignore', diffs.first.b_path + assert_equal '4ebc8ae', diffs.first.a_commit + assert_equal '2dd0253', diffs.first.b_commit + assert_equal '100644', diffs.first.mode + assert_equal false, diffs.first.new_file + assert_equal false, diffs.first.deleted_file + assert_equal "--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1,2 @@\n coverage\n+pkg", diffs.first.diff end # to_s diff --git a/test/test_git.rb b/test/test_git.rb index e615a035d096b6cbc984e2f4213c06d0ac785321..72a18ec424f078f6daee75dbc62265c02ba7a892 100644 --- a/test/test_git.rb +++ b/test/test_git.rb @@ -10,6 +10,12 @@ class TestGit < Test::Unit::TestCase end def test_transform_options + assert_equal ["-s"], @git.transform_options({:s => true}) + assert_equal ["-s 5"], @git.transform_options({:s => 5}) + + assert_equal ["--max-count"], @git.transform_options({:max_count => true}) assert_equal ["--max-count=5"], @git.transform_options({:max_count => 5}) + + assert_equal ["-t", "-s"], @git.transform_options({:s => true, :t => true}) end end \ No newline at end of file diff --git a/test/test_repo.rb b/test/test_repo.rb index d53476a51e3286be270c7b515ec1d65e5c1716e0..114a4464fa248550be10cc4abe0735d6025b5fca 100644 --- a/test/test_repo.rb +++ b/test/test_repo.rb @@ -59,9 +59,11 @@ class TestRepo < Test::Unit::TestCase assert_equal '4c8124ffcf4039d292442eeccabdeca5af5c5017', c.id assert_equal ["634396b2f541a9f2d58b00be1a07f0c358b999b3"], c.parents.map { |p| p.id } assert_equal "672eca9b7f9e09c22dcb128c283e8c3c8d7697a4", c.tree.id - assert_equal "Tom Preston-Werner ", c.author + assert_equal "Tom Preston-Werner", c.author.name + assert_equal "tom@mojombo.com", c.author.email assert_equal Time.at(1191999972), c.authored_date - assert_equal "Tom Preston-Werner ", c.committer + assert_equal "Tom Preston-Werner", c.committer.name + assert_equal "tom@mojombo.com", c.committer.email assert_equal Time.at(1191999972), c.committed_date assert_equal "implement Grit#heads", c.message @@ -125,4 +127,18 @@ class TestRepo < Test::Unit::TestCase def test_inspect assert_equal %Q{#}, @r.inspect end -end \ No newline at end of file + + # log + + def test_log + Git.any_instance.expects(:log).times(2).with({:pretty => 'raw'}, 'master').returns(fixture('rev_list')) + + assert_equal '4c8124ffcf4039d292442eeccabdeca5af5c5017', @r.log.first.id + assert_equal 'ab25fd8483882c3bda8a458ad2965d2248654335', @r.log.last.id + end + + def test_log_with_path_and_options + Git.any_instance.expects(:log).with({:pretty => 'raw', :max_count => 1}, 'master -- file.rb').returns(fixture('rev_list')) + @r.log('master', 'file.rb', :max_count => 1) + end +end