summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES7
-rw-r--r--LICENSE30
-rw-r--r--MANIFEST.in2
-rw-r--r--README212
-rw-r--r--VERSION1
-rw-r--r--ez_setup.py222
-rw-r--r--lib/git_python/__init__.py19
-rw-r--r--lib/git_python/actor.py33
-rw-r--r--lib/git_python/blob.py131
-rw-r--r--lib/git_python/commit.py235
-rw-r--r--lib/git_python/diff.py75
-rw-r--r--lib/git_python/errors.py5
-rw-r--r--lib/git_python/git.py72
-rw-r--r--lib/git_python/head.py107
-rw-r--r--lib/git_python/lazy.py26
-rw-r--r--lib/git_python/method_missing.py21
-rw-r--r--lib/git_python/repo.py417
-rw-r--r--lib/git_python/stats.py17
-rw-r--r--lib/git_python/tag.py85
-rw-r--r--lib/git_python/tree.py86
-rw-r--r--lib/git_python/utils.py8
-rw-r--r--setup.cfg3
-rw-r--r--setup.py76
-rw-r--r--test/__init__.py0
-rw-r--r--test/asserts.py32
-rw-r--r--test/fixtures/blame131
-rw-r--r--test/fixtures/cat_file_blob1
-rw-r--r--test/fixtures/cat_file_blob_size1
-rw-r--r--test/fixtures/diff_254
-rw-r--r--test/fixtures/diff_2f19
-rw-r--r--test/fixtures/diff_f15
-rw-r--r--test/fixtures/diff_i201
-rw-r--r--test/fixtures/diff_mode_only1152
-rw-r--r--test/fixtures/diff_new_mode14
-rw-r--r--test/fixtures/diff_numstat2
-rw-r--r--test/fixtures/diff_p610
-rw-r--r--test/fixtures/for_each_refbin0 -> 58 bytes
-rw-r--r--test/fixtures/for_each_ref_tagsbin0 -> 58 bytes
-rw-r--r--test/fixtures/ls_tree_a7
-rw-r--r--test/fixtures/ls_tree_b2
-rw-r--r--test/fixtures/ls_tree_commit3
-rw-r--r--test/fixtures/rev_list24
-rw-r--r--test/fixtures/rev_list_commit_diffs8
-rw-r--r--test/fixtures/rev_list_commit_idabbrev8
-rw-r--r--test/fixtures/rev_list_commit_stats7
-rw-r--r--test/fixtures/rev_list_count655
-rw-r--r--test/fixtures/rev_list_delta_a8
-rw-r--r--test/fixtures/rev_list_delta_b11
-rw-r--r--test/fixtures/rev_list_single7
-rw-r--r--test/fixtures/rev_parse1
-rw-r--r--test/fixtures/show_empty_commit6
-rw-r--r--test/git/__init__.py0
-rw-r--r--test/git/test_actor.py23
-rw-r--r--test/git/test_blob.py67
-rw-r--r--test/git/test_commit.py191
-rw-r--r--test/git/test_diff.py13
-rw-r--r--test/git/test_git.py49
-rw-r--r--test/git/test_head.py19
-rw-r--r--test/git/test_repo.py309
-rw-r--r--test/git/test_stats.py22
-rw-r--r--test/git/test_tag.py31
-rw-r--r--test/git/test_tree.py83
-rw-r--r--test/git/test_utils.py17
-rw-r--r--test/helper.py10
64 files changed, 5703 insertions, 0 deletions
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 00000000..ac7766e3
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,7 @@
+=======
+CHANGES
+=======
+
+0.1.0
+=====
+initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..a49d06b5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2008, Michael Trier
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+* Neither the name of the GitPython project nor the names of
+its contributors may be used to endorse or promote products derived
+from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..eeb770e0
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include doc/*.html
+include doc/*.css
diff --git a/README b/README
new file mode 100644
index 00000000..dffad2f0
--- /dev/null
+++ b/README
@@ -0,0 +1,212 @@
+==========
+GitPython
+==========
+
+GitPython is a python library used to interact with Git repositories.
+
+GitPython is a port of the grit_ library in Ruby created by
+Tom Preston-Werner and Chris Wanstrath.
+
+.. _grit: http://grit.rubyforge.org
+
+The ``method_missing`` stuff was `taken from this blog post`_
+
+.. _taken from this blog post: http://blog.iffy.us/?p=43
+
+REQUIREMENTS
+============
+
+* Git_ tested with 1.5.3.7
+* `Python Nose`_ - used for running the tests
+
+.. _Git: http://git.or.cz/
+.. _Python Nose: http://code.google.com/p/python-nose/
+
+INSTALL
+=======
+
+ python setup.py install
+
+SOURCE
+======
+
+GitPython's git repo is available on Gitorious, which can be browsed at:
+
+http://gitorious.org/projects/git-python
+
+and cloned from:
+
+git://gitorious.org/projects/git-python.git
+
+USAGE
+=====
+
+GitPython provides 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 `Repo` object to represent your repository.
+
+ >>> from git_python import *
+ >>> repo = Repo.new("/Users/mtrier/Development/git-python")
+
+In the above example, the directory `/Users/mtrier/Development/git-python` is my working
+repo and contains the `.git` directory. You can also initialize GitPython with a
+bare repo.
+
+ >>> repo = Repo.init_bare("/var/git/git-python.git")
+
+Getting a list of commits
+*************************
+
+From the `Repo` object, you can get a list of `Commit`
+objects.
+
+ >>> repo.commits()
+ [<GitPython.Commit "207c0c4418115df0d30820ab1a9acd2ea4bf4431">,
+ <GitPython.Commit "a91c45eee0b41bf3cdaad3418ca3850664c4a4b4">,
+ <GitPython.Commit "e17c7e11aed9e94d2159e549a99b966912ce1091">,
+ <GitPython.Commit "bd795df2d0e07d10e0298670005c0e9d9a5ed867">]
+
+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.
+
+ >>> repo.commits('mybranch')
+ >>> repo.commits('40d3057d09a7a4d61059bca9dca5ae698de58cbe')
+ >>> repo.commits('v0.1')
+
+You can specify the maximum number of commits to return.
+
+ >>> repo.commits('master', 100)
+
+If you need paging, you can specify a number of commits to skip.
+
+ >>> repo.commits('master', 10, 20)
+
+The above will return commits 21-30 from the commit list.
+
+The Commit object
+*****************
+
+Commit objects contain information about a specific commit.
+
+ >>> head = repo.commits()[0]
+
+ >>> head.id
+ '207c0c4418115df0d30820ab1a9acd2ea4bf4431'
+
+ >>> head.parents
+ [<GitPython.Commit "a91c45eee0b41bf3cdaad3418ca3850664c4a4b4">]
+
+ >>> head.tree
+ <GitPython.Tree "563413aedbeda425d8d9dcbb744247d0c3e8a0ac">
+
+ >>> head.author
+ <GitPython.Actor "Michael Trier <mtrier@gmail.com>">
+
+ >>> head.authored_date
+ (2008, 5, 7, 5, 0, 56, 2, 128, 0)
+
+ >>> head.committer
+ <GitPython.Actor "Michael Trier <mtrier@gmail.com>">
+
+ >>> head.committed_date
+ (2008, 5, 7, 5, 0, 56, 2, 128, 0)
+
+ >>> head.message
+ 'cleaned up a lot of test information. Fixed escaping so it works with subprocess.'
+
+
+You can traverse a commit's ancestry by chaining calls to ``parents``.
+
+ >>> repo.commits()[0].parents[0].parents[0].parents[0]
+
+The above corresponds to ``master^^^`` or ``master~3`` in git parlance.
+
+The Tree object
+***************
+
+A tree recorda 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()[0].tree
+ <GitPython.Tree "a006b5b1a8115185a228b7514cdcd46fed90dc92">
+
+ >>> tree.id
+ 'a006b5b1a8115185a228b7514cdcd46fed90dc92'
+
+Once you have a tree, you can get the contents.
+
+ >>> contents = tree.contents
+ [<GitPython.Blob "6a91a439ea968bf2f5ce8bb1cd8ddf5bf2cad6c7">,
+ <GitPython.Blob "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391">,
+ <GitPython.Tree "eaa0090ec96b054e425603480519e7cf587adfc3">,
+ <GitPython.Blob "980e72ae16b5378009ba5dfd6772b59fe7ccd2df">]
+
+This tree contains three ``Blob`` objects and one ``Tree`` object. The trees are
+subdirectories and the blobs are files. Trees below the root have additional
+attributes.
+
+ >>> contents = tree.contents[-2]
+ <GitPython.Tree "e5445b9db4a9f08d5b4de4e29e61dffda2f386ba">
+
+ >>> contents.name
+ 'test'
+
+ >>> contents.mode
+ '040000'
+
+There is a convenience method that allows you to get a named sub-object
+from a tree.
+
+ >>> tree/"lib"
+ <GitPython.Tree "c1c7214dde86f76bc3e18806ac1f47c38b2b7a30">
+
+You can also get a tree directly from the repo if you know its name.
+
+ >>> repo.tree()
+ <GitPython.Tree "master">
+
+ >>> repo.tree("c1c7214dde86f76bc3e18806ac1f47c38b2b7a30")
+ <GitPython.Tree "c1c7214dde86f76bc3e18806ac1f47c38b2b7a30">
+
+The Blob object
+***************
+
+A blob represents a file. Trees often contain blobs.
+
+ >>> blob = tree.contents[-1]
+ <GitPython.Blob "b19574431a073333ea09346eafd64e7b1908ef49">
+
+A blob has certain attributes.
+
+ >>> blob.name
+ 'urls.py'
+
+ >>> blob.mode
+ '100644'
+
+ >>> blob.mime_type
+ 'text/x-python'
+
+ >>> len(blob)
+ 415
+
+You can get the data of a blob as a string.
+
+ >>> blob.data
+ "from django.conf.urls.defaults import *\nfrom django.conf..."
+
+You can also get a blob directly from the repo if you know its name.
+
+ >>> repo.blob("b19574431a073333ea09346eafd64e7b1908ef49")
+ <GitPython.Blob "b19574431a073333ea09346eafd64e7b1908ef49">
+
+LICENSE
+=======
+
+New BSD License. See the LICENSE file. \ No newline at end of file
diff --git a/VERSION b/VERSION
new file mode 100644
index 00000000..6e8bf73a
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/ez_setup.py b/ez_setup.py
new file mode 100644
index 00000000..3031ad0d
--- /dev/null
+++ b/ez_setup.py
@@ -0,0 +1,222 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c3"
+DEFAULT_URL = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ from md5 import md5
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ try:
+ import setuptools
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+ except ImportError:
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+
+ import pkg_resources
+ try:
+ pkg_resources.require("setuptools>="+version)
+
+ except pkg_resources.VersionConflict, e:
+ # XXX could we install in a subprocess here?
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first.\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ # tell the user to uninstall obsolete version
+ use_setuptools(version)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+ from md5 import md5
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
diff --git a/lib/git_python/__init__.py b/lib/git_python/__init__.py
new file mode 100644
index 00000000..404cea12
--- /dev/null
+++ b/lib/git_python/__init__.py
@@ -0,0 +1,19 @@
+import inspect
+
+from actor import Actor
+from blob import Blob
+from commit import Commit
+from diff import Diff
+from errors import InvalidGitRepositoryError, NoSuchPathError
+from git import Git
+from head import Head
+from repo import Repo
+from stats import Stats
+from tag import Tag
+from tree import Tree
+from utils import shell_escape, dashify, touch
+
+__all__ = [ name for name, obj in locals().items()
+ if not (name.startswith('_') or inspect.ismodule(obj)) ]
+
+__version__ = 'svn'
diff --git a/lib/git_python/actor.py b/lib/git_python/actor.py
new file mode 100644
index 00000000..235c3e21
--- /dev/null
+++ b/lib/git_python/actor.py
@@ -0,0 +1,33 @@
+import re
+
+class Actor(object):
+ def __init__(self, name, email):
+ self.name = name
+ self.email = email
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return '<GitPython.Actor "%s <%s>">' % (self.name, self.email)
+
+ @classmethod
+ def from_string(cls, string):
+ """
+ Create an Actor from a string.
+
+ ``str``
+ is the string, which is expected to be in regular git format
+
+ Format
+ John Doe <jdoe@example.com>
+
+ Returns
+ Actor
+ """
+ if re.search(r'<.+>', string):
+ m = re.search(r'(.*) <(.+?)>', string)
+ name, email = m.groups()
+ return Actor(name, email)
+ else:
+ return Actor(string, None)
diff --git a/lib/git_python/blob.py b/lib/git_python/blob.py
new file mode 100644
index 00000000..c89c3c3f
--- /dev/null
+++ b/lib/git_python/blob.py
@@ -0,0 +1,131 @@
+import mimetypes
+import re
+import time
+from actor import Actor
+from commit import Commit
+
+class Blob(object):
+ DEFAULT_MIME_TYPE = "text/plain"
+
+ def __init__(self, repo, **kwargs):
+ """
+ Create an unbaked Blob containing just the specified attributes
+
+ ``repo``
+ is the Repo
+
+ ``atts``
+ is a dict of instance variable data
+
+ Returns
+ GitPython.Blob
+ """
+ self.id = None
+ self.mode = None
+ self.name = None
+ self.size = None
+ self.data_stored = None
+
+ self.repo = repo
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ def __len__(self):
+ """
+ The size of this blob in bytes
+
+ Returns
+ int
+ """
+ self.size = self.size or int(self.repo.git.cat_file(self.id, **{'s': True}).rstrip())
+ return self.size
+
+ @property
+ def data(self):
+ """
+ The binary contents of this blob.
+
+ Returns
+ str
+ """
+ self.data_stored = self.data_stored or self.repo.git.cat_file(self.id, **{'p': True})
+ return self.data_stored
+
+ @property
+ def mime_type(self):
+ """
+ The mime type of this file (based on the filename)
+
+ Returns
+ str
+ """
+ guesses = None
+ if self.name:
+ guesses = mimetypes.guess_type(self.name)
+ return guesses and guesses[0] or self.DEFAULT_MIME_TYPE
+
+ @property
+ def basename(self):
+ return os.path.basename(self.name)
+
+ @classmethod
+ def blame(cls, repo, commit, file):
+ """
+ The blame information for the given file at the given commit
+
+ Returns
+ list: [GitPython.Commit, list: [<line>]]
+ """
+ data = repo.git.blame(commit, '--', file, **{'p': True})
+ commits = {}
+ blames = []
+ info = None
+
+ for line in data.splitlines():
+ parts = re.split(r'\s+', line, 1)
+ if re.search(r'^[0-9A-Fa-f]{40}$', parts[0]):
+ if re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+) (\d+)$', line):
+ m = re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+) (\d+)$', line)
+ id, origin_line, final_line, group_lines = m.groups()
+ info = {'id': id}
+ blames.append([None, []])
+ elif re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+)$', line):
+ m = re.search(r'^([0-9A-Fa-f]{40}) (\d+) (\d+)$', line)
+ id, origin_line, final_line = m.groups()
+ info = {'id': id}
+ elif re.search(r'^(author|committer)', parts[0]):
+ if re.search(r'^(.+)-mail$', parts[0]):
+ m = re.search(r'^(.+)-mail$', parts[0])
+ info["%s_email" % m.groups()[0]] = parts[-1]
+ elif re.search(r'^(.+)-time$', parts[0]):
+ m = re.search(r'^(.+)-time$', parts[0])
+ info["%s_date" % m.groups()[0]] = time.gmtime(int(parts[-1]))
+ elif re.search(r'^(author|committer)$', parts[0]):
+ m = re.search(r'^(author|committer)$', parts[0])
+ info[m.groups()[0]] = parts[-1]
+ elif re.search(r'^filename', parts[0]):
+ info['filename'] = parts[-1]
+ elif re.search(r'^summary', parts[0]):
+ info['summary'] = parts[-1]
+ elif parts[0] == '':
+ if info:
+ c = commits.has_key(info['id']) and commits[info['id']]
+ if not c:
+ c = Commit(repo, **{'id': info['id'],
+ 'author': Actor.from_string(info['author'] + ' ' + info['author_email']),
+ 'authored_date': info['author_date'],
+ 'committer': Actor.from_string(info['committer'] + ' ' + info['committer_email']),
+ 'committed_date': info['committer_date'],
+ 'message': info['summary']})
+ commits[info['id']] = c
+
+ m = re.search(r'^\t(.*)$', line)
+ text, = m.groups()
+ blames[-1][0] = c
+ blames[-1][1] += text
+ info = None
+
+ return blames
+
+ def __repr__(self):
+ return '<GitPython.Blob "%s">' % self.id
diff --git a/lib/git_python/commit.py b/lib/git_python/commit.py
new file mode 100644
index 00000000..0cd7715e
--- /dev/null
+++ b/lib/git_python/commit.py
@@ -0,0 +1,235 @@
+import re
+import time
+
+from actor import Actor
+from lazy import LazyMixin
+import tree
+import diff
+import stats
+
+class Commit(LazyMixin):
+ def __init__(self, repo, **kwargs):
+ """
+ Instantiate a new Commit
+
+ ``id``
+ is the id of the commit
+
+ ``parents``
+ is a list 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 DateTime
+
+ ``committer``
+ is the committer string
+
+ ``committed_date``
+ is the committed DateTime
+
+ ``message``
+ is the first line of the commit message
+
+ Returns
+ GitPython.Commit
+ """
+ LazyMixin.__init__(self)
+
+ self.repo = repo
+ self.id = None
+ self.tree = None
+ self.author = None
+ self.authored_date = None
+ self.committer = None
+ self.committed_date = None
+ self.message = None
+ self.parents = None
+
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ if self.id:
+ if 'parents' in kwargs:
+ self.parents = map(lambda p: Commit(repo, **{'id': p}), kwargs['parents'])
+ if 'tree' in kwargs:
+ self.tree = tree.Tree(repo, **{'id': kwargs['tree']})
+
+ def __bake__(self):
+ temp = Commit.find_all(self.repo, self.id, **{'max_count': 1})[0]
+ self.parents = temp.parents
+ self.tree = temp.tree
+ self.author = temp.author
+ self.authored_date = temp.authored_date
+ self.committer = temp.committer
+ self.committed_date = temp.committed_date
+ self.message = temp.message
+
+ @property
+ def id_abbrev(self):
+ return self.id[0:7]
+
+ @classmethod
+ def count(cls, repo, ref):
+ """
+ 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
+ int
+ """
+ return len(repo.git.rev_list(ref).strip().splitlines())
+
+ @classmethod
+ def find_all(cls, repo, ref, **kwargs):
+ """
+ Find all commits matching the given criteria.
+ ``repo``
+ is the Repo
+
+ ``ref``
+ is the ref from which to begin (SHA1 or name)
+
+ ``options``
+ is a Hash of optional arguments to git where
+ ``max_count`` is the maximum number of commits to fetch
+ ``skip`` is the number of commits to skip
+
+ Returns
+ GitPython.Commit[]
+ """
+ options = {'pretty': 'raw'}
+ options.update(kwargs)
+
+ output = repo.git.rev_list(ref, **options)
+ return cls.list_from_string(repo, output)
+
+ @classmethod
+ def list_from_string(cls, repo, text):
+ """
+ Parse out commit information into a list of Commit objects
+
+ ``repo``
+ is the Repo
+
+ ``text``
+ is the text output from the git command (raw format)
+
+ Returns
+ GitPython.Commit[]
+ """
+ lines = [l for l in text.splitlines() if l.strip()]
+
+ commits = []
+
+ while lines:
+ id = lines.pop(0).split()[-1]
+ tree = lines.pop(0).split()[-1]
+
+ parents = []
+ while lines and re.search(r'^parent', lines[0]):
+ parents.append(lines.pop(0).split()[-1])
+ author, authored_date = cls.actor(lines.pop(0))
+ committer, committed_date = cls.actor(lines.pop(0))
+
+ messages = []
+ while lines and re.search(r'^ {4}', lines[0]):
+ messages.append(lines.pop(0).strip())
+
+ message = messages and messages[0] or ''
+
+ commits.append(Commit(repo, id=id, parents=parents, tree=tree, author=author, authored_date=authored_date,
+ committer=committer, committed_date=committed_date, message=message))
+
+ return commits
+
+ @classmethod
+ def diff(cls, repo, a, b = None, paths = []):
+ """
+ Show diffs between two trees:
+
+ ``repo``
+ is the Repo
+
+ ``a``
+ is a named commit
+
+ ``b``
+ is an optional named commit. Passing a list assumes you
+ wish to omit the second named commit and limit the diff to the
+ given paths.
+
+ ``paths``
+ is a list of paths to limit the diff.
+
+ Returns
+ GitPython.Diff[]
+ """
+ if isinstance(b, list):
+ paths = b
+ b = None
+
+ if paths:
+ paths.insert(0, "--")
+
+ if b:
+ paths.insert(0, b)
+ paths.insert(0, a)
+ text = repo.git.diff(*paths, **{'full_index': True})
+ return diff.Diff.list_from_string(repo, text)
+
+ @property
+ def diffs(self):
+ if not self.parents:
+ d = self.repo.git.show(self.id, **{'full_index': True, 'pretty': 'raw'})
+ if re.search(r'diff --git a', d):
+ if not re.search(r'^diff --git a', d):
+ p = re.compile(r'.+?(diff --git a)', re.MULTILINE | re.DOTALL)
+ d = p.sub(r'diff --git a', d, 1)
+ else:
+ d = ''
+ return diff.Diff.list_from_string(self.repo, d)
+ else:
+ return self.diff(self.repo, self.parents[0].id, self.id)
+
+ @property
+ def stats(self):
+ if not self.parents:
+ text = self.repo.git.diff(self.id, **{'numstat': True})
+ text2 = ""
+ for line in text.splitlines():
+ (insertions, deletions, filename) = line.split("\t")
+ text2 += "%s\t%s\t%s\n" % (deletions, insertions, filename)
+ text = text2
+ else:
+ text = self.repo.git.diff(self.parents[0].id, self.id, **{'numstat': True})
+ return stats.Stats.list_from_string(self.repo, text)
+
+ def __str__(self):
+ """ Convert commit to string which is SHA1 """
+ return self.id
+
+ def __repr__(self):
+ return '<GitPython.Commit "%s">' % self.id
+
+ @classmethod
+ def actor(cls, line):
+ """
+ Parse out the actor (author or committer) info
+
+ Returns
+ [str (actor name and email), time (acted at time)]
+ """
+ m = re.search(r'^.+? (.*) (\d+) .*$', line)
+ actor, epoch = m.groups()
+ return [Actor.from_string(actor), time.gmtime(int(epoch))]
diff --git a/lib/git_python/diff.py b/lib/git_python/diff.py
new file mode 100644
index 00000000..9d2690be
--- /dev/null
+++ b/lib/git_python/diff.py
@@ -0,0 +1,75 @@
+import re
+import commit
+
+class Diff(object):
+ def __init__(self, repo, a_path, b_path, a_commit, b_commit, a_mode, b_mode, new_file, deleted_file, diff):
+ self.repo = repo
+ self.a_path = a_path
+ self.b_path = b_path
+
+ if not a_commit or re.search(r'^0{40}$', a_commit):
+ self.a_commit = None
+ else:
+ self.a_commit = commit.Commit(repo, **{'id': a_commit})
+ if not b_commit or re.search(r'^0{40}$', b_commit):
+ self.b_commit = None
+ else:
+ self.b_commit = commit.Commit(repo, **{'id': b_commit})
+
+ self.a_mode = a_mode
+ self.b_mode = b_mode
+ self.new_file = new_file
+ self.deleted_file = deleted_file
+ self.diff = diff
+
+ @classmethod
+ def list_from_string(cls, repo, text):
+ lines = text.splitlines()
+ a_mode = None
+ b_mode = None
+ diffs = []
+ while lines:
+ m = re.search(r'^diff --git a/(\S+) b/(\S+)$', lines.pop(0))
+ if m:
+ a_path, b_path = m.groups()
+ if re.search(r'^old mode', lines[0]):
+ m = re.search(r'^old mode (\d+)', lines.pop(0))
+ if m:
+ a_mode, = m.groups()
+ m = re.search(r'^new mode (\d+)', lines.pop(0))
+ if m:
+ b_mode, = m.groups()
+ if re.search(r'^diff --git', lines[0]):
+ diffs.append(Diff(repo, a_path, b_path, None, None, a_mode, b_mode, False, False, None))
+ continue
+
+ new_file = False
+ deleted_file = False
+
+ if re.search(r'^new file', lines[0]):
+ m = re.search(r'^new file mode (.+)', lines.pop(0))
+ if m:
+ b_mode, = m.groups()
+ a_mode = None
+ new_file = True
+ elif re.search(r'^deleted file', lines[0]):
+ m = re.search(r'^deleted file mode (.+)$', lines.pop(0))
+ if m:
+ a_mode, = m.groups()
+ b_mode = None
+ deleted_file = True
+
+ m = re.search(r'^index ([0-9A-Fa-f]+)\.\.([0-9A-Fa-f]+) ?(.+)?$', lines.pop(0))
+ if m:
+ a_commit, b_commit, b_mode = m.groups()
+ if b_mode:
+ b_mode = b_mode.strip()
+
+ diff_lines = []
+ while lines and not re.search(r'^diff', lines[0]):
+ diff_lines.append(lines.pop(0))
+
+ diff = "\n".join(diff_lines)
+ diffs.append(Diff(repo, a_path, b_path, a_commit, b_commit, a_mode, b_mode, new_file, deleted_file, diff))
+
+ return diffs \ No newline at end of file
diff --git a/lib/git_python/errors.py b/lib/git_python/errors.py
new file mode 100644
index 00000000..4083841e
--- /dev/null
+++ b/lib/git_python/errors.py
@@ -0,0 +1,5 @@
+class InvalidGitRepositoryError(Exception):
+ pass
+
+class NoSuchPathError(Exception):
+ pass
diff --git a/lib/git_python/git.py b/lib/git_python/git.py
new file mode 100644
index 00000000..4bc8760a
--- /dev/null
+++ b/lib/git_python/git.py
@@ -0,0 +1,72 @@
+import os
+import subprocess
+import re
+from utils import *
+from method_missing import MethodMissingMixin
+
+class Git(MethodMissingMixin):
+ def __init__(self, git_dir):
+ super(Git, self).__init__()
+ self.git_dir = git_dir
+
+ git_binary = "/usr/bin/env git"
+
+ @property
+ def get_dir(self):
+ return self.git_dir
+
+ def execute(self, command):
+ print command
+ proc = subprocess.Popen(command,
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ stdout_value = proc.communicate()[0]
+ return stdout_value
+
+ def transform_kwargs(self, **kwargs):
+ """
+ Transforms Python style kwargs into git command line options.
+ """
+ args = []
+ for k, v in kwargs.items():
+ if len(k) == 1:
+ if v is True:
+ args.append("-%s" % k)
+ else:
+ args.append("-%s %r" % (k, v))
+ else:
+ if v is True:
+ args.append("--%s" % dashify(k))
+ else:
+ args.append("--%s=%r" % (dashify(k), v))
+ return args
+
+ def method_missing(self, method, *args, **kwargs):
+ """
+ Run the given git command with the specified arguments and return
+ the result as a String
+
+ ``method``
+ is the command
+
+ ``args``
+ is the list of arguments
+
+ ``kwargs``
+ is a dict of keyword arguments
+
+ Examples
+ git.rev_list('master', max_count=10, header=True)
+
+ Returns
+ str
+ """
+ opt_args = self.transform_kwargs(**kwargs)
+ ext_args = map(lambda a: (a == '--') and a or "%s" % shell_escape(a), args)
+ args = opt_args + ext_args
+
+ call = "%s --git-dir=%s %s %s" % (self.git_binary, self.git_dir, dashify(method), ' '.join(args))
+ stdout_value = self.execute(call)
+ return stdout_value
diff --git a/lib/git_python/head.py b/lib/git_python/head.py
new file mode 100644
index 00000000..cb432bd9
--- /dev/null
+++ b/lib/git_python/head.py
@@ -0,0 +1,107 @@
+import commit
+
+class Head(object):
+ """
+ A Head is a named reference to a Commit. Every Head instance contains a name
+ and a Commit object.
+
+ Examples::
+
+ >>> repo = Repo("/path/to/repo")
+ >>> head = repo.heads[0]
+
+ >>> head.name
+ 'master'
+
+ >>> head.commit
+ <GitPython.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455">
+
+ >>> head.commit.id
+ '1c09f116cbc2cb4100fb6935bb162daa4723f455'
+ """
+
+ def __init__(self, name, commit):
+ """
+ Instantiate a new Head
+
+ `name`
+ is the name of the head
+
+ `commit`
+ is the Commit that the head points to
+
+ Returns
+ GitPython.Head
+ """
+ self.name = name
+ self.commit = commit
+
+ @classmethod
+ def find_all(cls, repo, **kwargs):
+ """
+ Find all Heads
+
+ `repo`
+ is the Repo
+
+ `kwargs`
+ is a dict of options
+
+ Returns
+ GitPython.Head[]
+ """
+
+ options = {'sort': "committerdate",
+ 'format': "%(refname)%00%(objectname)"}
+ options.update(kwargs)
+
+ output = repo.git.for_each_ref("refs/heads", **options)
+ return cls.list_from_string(repo, output)
+
+ @classmethod
+ def list_from_string(cls, repo, text):
+ """
+ Parse out head information into an array of baked head objects
+
+ ``repo``
+ is the Repo
+ ``text``
+ is the text output from the git command
+
+ Returns
+ GitPython.Head[]
+ """
+ heads = []
+
+ for line in text.splitlines():
+ heads.append(cls.from_string(repo, line))
+
+ return heads
+
+ @classmethod
+ def from_string(cls, repo, line):
+ """
+ Create a new Head instance from the given string.
+
+ ``repo``
+ is the Repo
+
+ ``line``
+ is the formatted head information
+
+ Format
+ name: [a-zA-Z_/]+
+ <null byte>
+ id: [0-9A-Fa-f]{40}
+
+ Returns
+ GitPython.Head
+ """
+ print line
+ full_name, ids = line.split("\x00")
+ name = full_name.split("/")[-1]
+ c = commit.Commit(repo, **{'id': ids})
+ return Head(name, c)
+
+ def __repr__(self):
+ return '<GitPython.Head "%s">' % self.name
diff --git a/lib/git_python/lazy.py b/lib/git_python/lazy.py
new file mode 100644
index 00000000..f6d31e8d
--- /dev/null
+++ b/lib/git_python/lazy.py
@@ -0,0 +1,26 @@
+class LazyMixin(object):
+ lazy_properties = []
+
+ def __init__(self):
+ self.__baked__ = False
+
+ def __getattribute__(self, attr):
+ val = object.__getattribute__(self, attr)
+ if val is not None:
+ return val
+ else:
+ self.__prebake__()
+ return object.__getattribute__(self, attr)
+
+ def __bake__(self):
+ """ This method should be overridden in the derived class. """
+ raise NotImplementedError(" '__bake__' method has not been implemented.")
+
+ def __prebake__(self):
+ if self.__baked__:
+ return
+ self.__bake__()
+ self.__baked__ = True
+
+ def __bake_it__(self):
+ self.__baked__ = True
diff --git a/lib/git_python/method_missing.py b/lib/git_python/method_missing.py
new file mode 100644
index 00000000..63bc36a9
--- /dev/null
+++ b/lib/git_python/method_missing.py
@@ -0,0 +1,21 @@
+class MethodMissingMixin(object):
+ """
+ A Mixin' to implement the 'method_missing' Ruby-like protocol.
+
+ This was `taken from a blog post <http://blog.iffy.us/?p=43>`_
+ """
+ def __getattribute__(self, attr):
+ try:
+ return object.__getattribute__(self, attr)
+ except:
+ class MethodMissing(object):
+ def __init__(self, wrapped, method):
+ self.__wrapped__ = wrapped
+ self.__method__ = method
+ def __call__(self, *args, **kwargs):
+ return self.__wrapped__.method_missing(self.__method__, *args, **kwargs)
+ return MethodMissing(self, attr)
+
+ def method_missing(self, *args, **kwargs):
+ """ This method should be overridden in the derived class. """
+ raise NotImplementedError(str(self.__wrapped__) + " 'method_missing' method has not been implemented.")
diff --git a/lib/git_python/repo.py b/lib/git_python/repo.py
new file mode 100644
index 00000000..a9bf30dd
--- /dev/null
+++ b/lib/git_python/repo.py
@@ -0,0 +1,417 @@
+import os
+import re
+from errors import InvalidGitRepositoryError, NoSuchPathError
+from utils import touch
+from git import Git
+from head import Head
+from blob import Blob
+from tag import Tag
+from commit import Commit
+from tree import Tree
+
+class Repo(object):
+ DAEMON_EXPORT_FILE = 'git-daemon-export-ok'
+
+ def __init__(self, path):
+ """
+ Create a new Repo instance
+
+ ``path``
+ is the path to either the root git directory or the bare git repo
+
+ Examples::
+
+ repo = Repo("/Users/mtrier/Development/git-python")
+ repo = Repo("/Users/mtrier/Development/git-python.git")
+
+ Returns
+ ``GitPython.Repo``
+ """
+ epath = os.path.abspath(path)
+
+ if os.path.exists(os.path.join(epath, '.git')):
+ self.path = os.path.join(epath, '.git')
+ self.bare = False
+ elif os.path.exists(epath) and re.search('\.git$', epath):
+ self.path = epath
+ self.bare = True
+ elif os.path.exists(epath):
+ raise InvalidGitRepositoryError(epath)
+ else:
+ raise NoSuchPathError(epath)
+ self.git = Git(self.path)
+
+ @property
+ def description(self):
+ """
+ The project's description. Taken verbatim from GIT_REPO/description
+
+ Returns
+ str
+ """
+ try:
+ f = open(os.path.join(self.path, 'description'))
+ result = f.read()
+ return result.rstrip()
+ finally:
+ f.close()
+
+ @property
+ def heads(self):
+ """
+ A list of ``Head`` objects representing the branch heads in
+ this repo
+
+ Returns
+ ``GitPython.Head[]``
+ """
+ return Head.find_all(self)
+
+ # alias heads
+ branches = heads
+
+ @property
+ def tags(self):
+ """
+ A list of ``Tag`` objects that are available in this repo
+
+ Returns
+ ``GitPython.Tag[]``
+ """
+ return Tag.find_all(self)
+
+ def commits(self, start = 'master', max_count = 10, skip = 0):
+ """
+ A list of Commit objects representing the history of a given ref/commit
+
+ ``start``
+ is the branch/commit name (default 'master')
+
+ ``max_count``
+ is the maximum number of commits to return (default 10)
+
+ ``skip``
+ is the number of commits to skip (default 0)
+
+ Returns
+ ``GitPython.Commit[]``
+ """
+ options = {'max_count': max_count,
+ 'skip': skip}
+
+ return Commit.find_all(self, start, **options)
+
+ def commits_between(self, frm, to):
+ """
+ The Commits objects that are reachable via ``to`` but not via ``frm``
+ Commits are returned in chronological order.
+
+ ``from``
+ is the branch/commit name of the younger item
+
+ ``to``
+ is the branch/commit name of the older item
+
+ Returns
+ ``GitPython.Commit[]``
+ """
+ return Commit.find_all(self, "%s..%s" % (frm, to)).reverse()
+
+ def commits_since(self, start = 'master', since = '1970-01-01'):
+ """
+ The Commits objects that are newer than the specified date.
+ Commits are returned in chronological order.
+
+ ``start``
+ is the branch/commit name (default 'master')
+
+ ``since``
+ is a string represeting a date/time
+
+ Returns
+ ``GitPython.Commit[]``
+ """
+ options = {'since': since}
+
+ return Commit.find_all(self, start, **options)
+
+ def commit_count(self, start = 'master'):
+ """
+ The number of commits reachable by the given branch/commit
+
+ ``start``
+ is the branch/commit name (default 'master')
+
+ Returns
+ int
+ """
+ return Commit.count(self, start)
+
+ def commit(self, id):
+ """
+ The Commit object for the specified id
+
+ ``id``
+ is the SHA1 identifier of the commit
+
+ Returns
+ GitPython.Commit
+ """
+ options = {'max_count': 1}
+
+ return Commit.find_all(self, id, **options)[0]
+
+ def commit_deltas_from(self, other_repo, ref = 'master', other_ref = 'master'):
+ """
+ Returns a list of commits that is in ``other_repo`` but not in self
+
+ Returns
+ ``GitPython.Commit[]``
+ """
+ repo_refs = self.git.rev_list(ref).strip().splitlines()
+ other_repo_refs = other_repo.git.rev_list(other_ref).strip().splitlines()
+
+ diff_refs = list(set(other_repo_refs) - set(repo_refs))
+ return map(lambda ref: Commit.find_all(other_repo, ref, **{'max_count': 1}[0]), diff_refs)
+
+ def tree(self, treeish = 'master', paths = []):
+ """
+ The Tree object for the given treeish reference
+
+ ``treeish``
+ is the reference (default 'master')
+ ``paths``
+ is an optional Array of directory paths to restrict the tree (deafult [])
+
+ Examples::
+
+ repo.tree('master', ['lib/'])
+
+
+ Returns
+ ``GitPython.Tree``
+ """
+ return Tree.construct(self, treeish, paths)
+
+ def blob(self, id):
+ """
+ The Blob object for the given id
+
+ ``id``
+ is the SHA1 id of the blob
+
+ Returns
+ ``GitPython.Blob``
+ """
+ return Blob(self, **{'id': id})
+
+ def log(self, commit = 'master', path = None, **kwargs):
+ """
+ The commit log for a treeish
+
+ Returns
+ ``GitPython.Commit[]``
+ """
+ options = {'pretty': 'raw'}
+ options.update(kwargs)
+ if path:
+ arg = [commit, '--', path]
+ else:
+ arg = [commit]
+ commits = self.git.log(*arg, **options)
+ return Commit.list_from_string(self, commits)
+
+ def diff(self, a, b, *paths):
+ """
+ The diff from commit ``a`` to commit ``b``, optionally restricted to the given file(s)
+
+ ``a``
+ is the base commit
+ ``b``
+ is the other commit
+
+ ``paths``
+ is an optional list of file paths on which to restrict the diff
+ """
+ self.git.diff(a, b, '--', *paths)
+
+ def commit_diff(self, commit):
+ """
+ The commit diff for the given commit
+ ``commit`` is the commit name/id
+
+ Returns
+ ``GitPython.Diff[]``
+ """
+ return Commit.diff(self, commit)
+
+ @classmethod
+ def init_bare(self, path, **kwargs):
+ """
+ Initialize a bare git repository at the given path
+
+ ``path``
+ is the full path to the repo (traditionally ends with /<name>.git)
+
+ ``kwargs``
+ is any additional options to the git init command
+
+ Examples::
+
+ GitPython.Repo.init_bare('/var/git/myrepo.git')
+
+ Returns
+ ``GitPython.Repo`` (the newly created repo)
+ """
+ git = Git(path)
+ git.init(**kwargs)
+ return Repo(path)
+
+ def fork_bare(self, path, **kwargs):
+ """
+ Fork a bare git repository from this repo
+
+ ``path``
+ is the full path of the new repo (traditionally ends with /<name>.git)
+
+ ``options``
+ is any additional options to the git clone command
+
+ Returns
+ ``GitPython.Repo`` (the newly forked repo)
+ """
+ options = {'bare': True, 'shared': False}
+ options.update(kwargs)
+ self.git.clone(self.path, path, **options)
+ return Repo(path)
+
+ def archive_tar(self, treeish = 'master', prefix = None):
+ """
+ Archive the given treeish
+
+ ``treeish``
+ is the treeish name/id (default 'master')
+
+ ``prefix``
+ is the optional prefix
+
+ Examples::
+
+ >>> repo.archive_tar
+ <String containing tar archive>
+
+ >>> repo.archive_tar('a87ff14')
+ <String containing tar archive for commit a87ff14>
+
+ >>> repo.archive_tar('master', 'myproject/')
+ <String containing tar archive and prefixed with 'myproject/'>
+
+ Returns
+ str (containing tar archive)
+ """
+ options = {}
+ if prefix:
+ options['prefix'] = prefix
+ return self.git.archive(treeish, **options)
+
+ def archive_tar_gz(self, treeish = 'master', prefix = None):
+ """
+ Archive and gzip the given treeish
+
+ ``treeish``
+ is the treeish name/id (default 'master')
+
+ ``prefix``
+ is the optional prefix
+
+ Examples::
+
+ >>> repo.archive_tar_gz
+ <String containing tar.gz archive>
+
+ >>> repo.archive_tar_gz('a87ff14')
+ <String containing tar.gz archive for commit a87ff14>
+
+ >>> repo.archive_tar_gz('master', 'myproject/')
+ <String containing tar.gz archive and prefixed with 'myproject/'>
+
+ Returns
+ str (containing tar.gz archive)
+ """
+ kwargs = {}
+ if prefix:
+ kwargs['prefix'] = prefix
+ self.git.archive(treeish, "| gzip", **kwargs)
+
+ def enable_daemon_serve(self):
+ """
+ Enable git-daemon serving of this repository by writing the
+ git-daemon-export-ok file to its git directory
+
+ Returns
+ None
+ """
+ if self.bare:
+ touch(os.path.join(self.path, DAEMON_EXPORT_FILE))
+ else:
+ touch(os.path.join(self.path, '.git', DAEMON_EXPORT_FILE))
+
+ def disable_daemon_serve(self):
+ """
+ Disable git-daemon serving of this repository by ensuring there is no
+ git-daemon-export-ok file in its git directory
+
+ Returns
+ None
+ """
+ if self.bare:
+ return os.remove(os.path.join(self.path, DAEMON_EXPORT_FILE))
+ else:
+ return os.remove(os.path.join(self.path, '.git', DAEMON_EXPORT_FILE))
+
+ def get_alternates(self):
+ """
+ The list of alternates for this repo
+
+ Returns
+ list[str] (pathnames of alternates)
+ """
+ alternates_path = os.path.join(self.path, *['objects', 'info', 'alternates'])
+
+ if os.path.exists(alternates_path):
+ try:
+ f = open(alternates_path)
+ alts = f.read()
+ finally:
+ f.close()
+ return alts.strip().splitlines()
+ else:
+ return []
+
+ def set_alternates(self, alts):
+ """
+ Sets the alternates
+
+ ``alts``
+ is the Array of String paths representing the alternates
+
+ Returns
+ None
+ """
+ for alt in alts:
+ if not os.path.exists(alt):
+ raise NoSuchPathError("Could not set alternates. Alternate path %s must exist" % alt)
+
+ if not alts:
+ os.remove(os.path.join(self.path, *['objects', 'info', 'alternates']))
+ else:
+ try:
+ f = open(os.path.join(self.path, *['objects', 'info', 'alternates']), 'w')
+ f.write("\n".join(alts))
+ finally:
+ f.close()
+
+ alternates = property(get_alternates, set_alternates)
+
+ def __repr__(self):
+ return '<GitPython.Repo "%s">' % self.path
diff --git a/lib/git_python/stats.py b/lib/git_python/stats.py
new file mode 100644
index 00000000..f1e10348
--- /dev/null
+++ b/lib/git_python/stats.py
@@ -0,0 +1,17 @@
+class Stats(object):
+ def __init__(self, repo, total, files):
+ self.repo = repo
+ self.total = total
+ self.files = files
+
+ @classmethod
+ def list_from_string(cls, repo, text):
+ hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': {}}
+ for line in text.splitlines():
+ (insertions, deletions, filename) = line.split("\t")
+ hsh['total']['insertions'] += int(insertions)
+ hsh['total']['deletions'] += int(deletions)
+ hsh['total']['lines'] = (hsh['total']['deletions'] + hsh['total']['insertions'])
+ hsh['total']['files'] += 1
+ hsh['files'][filename.strip()] = {'insertions': int(insertions), 'deletions': int(deletions)}
+ return Stats(repo, hsh['total'], hsh['files']) \ No newline at end of file
diff --git a/lib/git_python/tag.py b/lib/git_python/tag.py
new file mode 100644
index 00000000..7da48d85
--- /dev/null
+++ b/lib/git_python/tag.py
@@ -0,0 +1,85 @@
+from commit import Commit
+
+class Tag(object):
+ def __init__(self, name, commit):
+ """
+ Instantiate a new Tag
+
+ ``name``
+ is the name of the head
+
+ ``commit``
+ is the Commit that the head points to
+
+ Returns
+ ``GitPython.Tag``
+ """
+ self.name = name
+ self.commit = commit
+
+ @classmethod
+ def find_all(cls, repo, **kwargs):
+ """
+ Find all Tags
+
+ ``repo``
+ is the Repo
+
+ ``kwargs``
+ is a dict of options
+
+ Returns
+ ``GitPython.Tag[]``
+ """
+ options = {'sort': "committerdate",
+ 'format': "%(refname)%00%(objectname)"}
+ options.update(**kwargs)
+
+ output = repo.git.for_each_ref("refs/tags", **options)
+ return cls.list_from_string(repo, output)
+
+ @classmethod
+ def list_from_string(cls, repo, text):
+ """
+ Parse out tag information into an array of baked Tag objects
+
+ ``repo``
+ is the Repo
+
+ ``text``
+ is the text output from the git command
+
+ Returns
+ ``GitPython.Tag[]``
+ """
+ tags = []
+ for line in text.splitlines():
+ tags.append(cls.from_string(repo, line))
+ return tags
+
+ @classmethod
+ def from_string(cls, repo, line):
+ """
+ Create a new Tag instance from the given string.
+
+ ``repo``
+ is the Repo
+
+ ``line``
+ is the formatted tag information
+
+ Format
+ name: [a-zA-Z_/]+
+ <null byte>
+ id: [0-9A-Fa-f]{40}
+
+ Returns
+ ``GitPython.Tag``
+ """
+ full_name, ids = line.split("\x00")
+ name = full_name.split("/")[-1]
+ commit = Commit(repo, **{'id': ids})
+ return Tag(name, commit)
+
+ def __repr__(self):
+ return '<GitPython.Tag "%s">' % self.name
diff --git a/lib/git_python/tree.py b/lib/git_python/tree.py
new file mode 100644
index 00000000..e1afc7ff
--- /dev/null
+++ b/lib/git_python/tree.py
@@ -0,0 +1,86 @@
+from lazy import LazyMixin
+import blob
+
+class Tree(LazyMixin):
+ def __init__(self, repo, **kwargs):
+ LazyMixin.__init__(self)
+ self.repo = repo
+ self.id = None
+ self.contents = None
+
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ def __bake__(self):
+ temp = Tree.construct(self.repo, self.id)
+ self.contents = temp.contents
+
+ @classmethod
+ def construct(cls, repo, treeish, paths = []):
+ output = repo.git.ls_tree(treeish, *paths)
+ return Tree(repo, **{'id': treeish}).construct_initialize(repo, treeish, output)
+
+ def construct_initialize(self, repo, id, text):
+ self.repo = repo
+ self.id = id
+ self.contents = []
+ self.__baked__ = False
+
+ for line in text.splitlines():
+ self.contents.append(self.content_from_string(self.repo, line))
+
+ self.contents = [c for c in self.contents if c is not None]
+
+ self.__bake_it__()
+ return self
+
+ def content_from_string(self, repo, text):
+ """
+ Parse a content item and create the appropriate object
+
+ ``repo``
+ is the Repo
+
+ ``text``
+ is the single line containing the items data in `git ls-tree` format
+
+ Returns
+ ``GitPython.Blob`` or ``GitPython.Tree``
+ """
+ try:
+ mode, typ, id, name = text.expandtabs(1).split(" ", 4)
+ except:
+ return None
+
+ if typ == "tree":
+ return Tree(repo, **{'id': id, 'mode': mode, 'name': name})
+ elif typ == "blob":
+ return blob.Blob(repo, **{'id': id, 'mode': mode, 'name': name})
+ elif typ == "commit":
+ return None
+ else:
+ raise(TypeError, "Invalid type: %s" % typ)
+
+ def __div__(self, file):
+ """
+ Find the named object in this tree's contents
+
+ Examples::
+
+ >>> Repo('/path/to/python-git').tree/'lib'
+ <GitPython.Tree "6cc23ee138be09ff8c28b07162720018b244e95e">
+ >>> Repo('/path/to/python-git').tree/'README.txt'
+ <GitPython.Blob "8b1e02c0fb554eed2ce2ef737a68bb369d7527df">
+
+ Returns
+ ``GitPython.Blob`` or ``GitPython.Tree`` or ``None`` if not found
+ """
+ contents = [c for c in self.contents if c.name == file]
+ return contents and contents[0] or None
+
+ @property
+ def basename(self):
+ os.path.basename(self.name)
+
+ def __repr__(self):
+ return '<GitPython.Tree "%s">' % self.id
diff --git a/lib/git_python/utils.py b/lib/git_python/utils.py
new file mode 100644
index 00000000..2bd6f2cd
--- /dev/null
+++ b/lib/git_python/utils.py
@@ -0,0 +1,8 @@
+def shell_escape(string):
+ return str(string).replace("'", "\\\\'")
+
+def dashify(string):
+ return string.replace('_', '-')
+
+def touch(filename):
+ open(filename, "a").close()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..01bb9544
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..008fb668
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,76 @@
+from ez_setup import use_setuptools
+use_setuptools()
+from setuptools import setup, find_packages
+from distutils.command.build_py import build_py as _build_py
+from setuptools.command.sdist import sdist as _sdist
+import os
+from os import path
+
+v = open(path.join(path.dirname(__file__), 'VERSION'))
+VERSION = v.readline().strip()
+v.close()
+
+class build_py(_build_py):
+ def run(self):
+ init = path.join(self.build_lib, 'git_python', '__init__.py')
+ if path.exists(init):
+ os.unlink(init)
+ _build_py.run(self)
+ _stamp_version(init)
+ self.byte_compile([init])
+
+class sdist(_sdist):
+ def make_release_tree (self, base_dir, files):
+ _sdist.make_release_tree(self, base_dir, files)
+ orig = path.join('lib', 'git_python', '__init__.py')
+ assert path.exists(orig)
+ dest = path.join(base_dir, orig)
+ if hasattr(os, 'link') and path.exists(dest):
+ os.unlink(dest)
+ self.copy_file(orig, dest)
+ _stamp_version(dest)
+
+def _stamp_version(filename):
+ found, out = False, []
+ f = open(filename, 'r')
+ for line in f:
+ if '__version__ =' in line:
+ line = line.replace("'svn'", "'%s'" % VERSION)
+ found = True
+ out.append(line)
+ f.close()
+
+ if found:
+ f = open(filename, 'w')
+ f.writelines(out)
+ f.close()
+
+
+setup(name = "GitPython",
+ cmdclass={'build_py': build_py, 'sdist': sdist},
+ version = VERSION,
+ description = "Python Git Library",
+ author = "Michael Trier",
+ author_email = "mtrier@gmail.com",
+ url = "http://gitalicious.org/projects/git-python/",
+ packages = find_packages('lib'),
+ package_dir = {'':'lib'},
+ license = "BSD License",
+ long_description = """\
+GitPython is a python library used to interact with Git repositories.
+
+GitPython provides 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.
+
+GitPython is a port of the grit library in Ruby created by
+Tom Preston-Werner and Chris Wanstrath.
+""",
+ classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ]
+ )
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/__init__.py
diff --git a/test/asserts.py b/test/asserts.py
new file mode 100644
index 00000000..33dd843b
--- /dev/null
+++ b/test/asserts.py
@@ -0,0 +1,32 @@
+import re
+import unittest
+from nose import tools
+from nose.tools import *
+
+__all__ = ['assert_instance_of', 'assert_not_instance_of',
+ 'assert_none', 'assert_not_none',
+ 'assert_match', 'assert_not_match'] + tools.__all__
+
+def assert_instance_of(expected, actual, msg=None):
+ """Verify that object is an instance of expected """
+ assert isinstance(actual, expected), msg
+
+def assert_not_instance_of(expected, actual, msg=None):
+ """Verify that object is not an instance of expected """
+ assert not isinstance(actual, expected, msg)
+
+def assert_none(actual, msg=None):
+ """verify that item is None"""
+ assert_equal(None, actual, msg)
+
+def assert_not_none(actual, msg=None):
+ """verify that item is None"""
+ assert_not_equal(None, actual, msg)
+
+def assert_match(pattern, string, msg=None):
+ """verify that the pattern matches the string"""
+ assert_not_none(re.search(pattern, string), msg)
+
+def assert_not_match(pattern, string, msg=None):
+ """verify that the pattern does not match the string"""
+ assert_none(re.search(pattern, string), msg) \ No newline at end of file
diff --git a/test/fixtures/blame b/test/fixtures/blame
new file mode 100644
index 00000000..10c141dd
--- /dev/null
+++ b/test/fixtures/blame
@@ -0,0 +1,131 @@
+634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1191997100
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1191997100
+committer-tz -0700
+filename lib/grit.rb
+summary initial grit setup
+boundary
+ $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
+634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2
+
+634396b2f541a9f2d58b00be1a07f0c358b999b3 3 3
+ # core
+634396b2f541a9f2d58b00be1a07f0c358b999b3 4 4
+
+634396b2f541a9f2d58b00be1a07f0c358b999b3 5 5
+ # stdlib
+634396b2f541a9f2d58b00be1a07f0c358b999b3 6 6
+
+634396b2f541a9f2d58b00be1a07f0c358b999b3 7 7
+ # internal requires
+3b1930208a82457747d76729ae088e90edca4673 8 8 1
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1192267241
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1192267241
+committer-tz -0700
+filename lib/grit.rb
+summary big refactor to do lazy loading
+ require 'grit/lazy'
+4c8124ffcf4039d292442eeccabdeca5af5c5017 8 9 1
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1191999972
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1191999972
+committer-tz -0700
+filename lib/grit.rb
+summary implement Grit#heads
+ require 'grit/errors'
+d01a4cfad6ea50285c4710243e3cbe019d381eba 9 10 1
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1192032303
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1192032303
+committer-tz -0700
+filename lib/grit.rb
+summary convert to Grit module, refactor to be more OO
+ require 'grit/git'
+4c8124ffcf4039d292442eeccabdeca5af5c5017 9 11 1
+ require 'grit/head'
+a47fd41f3aa4610ea527dcc1669dfdb9c15c5425 10 12 1
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1192002639
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1192002639
+committer-tz -0700
+filename lib/grit.rb
+summary add more comments throughout
+ require 'grit/commit'
+b17b974691f0a26f26908495d24d9c4c718920f8 13 13 1
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1192271832
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1192271832
+committer-tz -0700
+filename lib/grit.rb
+summary started implementing Tree
+ require 'grit/tree'
+74fd66519e983a0f29e16a342a6059dbffe36020 14 14 1
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1192317005
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1192317005
+committer-tz -0700
+filename lib/grit.rb
+summary add Blob
+ require 'grit/blob'
+d01a4cfad6ea50285c4710243e3cbe019d381eba 12 15 1
+ require 'grit/repo'
+634396b2f541a9f2d58b00be1a07f0c358b999b3 9 16 1
+
+d01a4cfad6ea50285c4710243e3cbe019d381eba 14 17 1
+ module Grit
+b6e1b765e0c15586a2c5b9832854f95defd71e1f 18 18 6
+author Tom Preston-Werner
+author-mail <tom@mojombo.com>
+author-time 1192860483
+author-tz -0700
+committer Tom Preston-Werner
+committer-mail <tom@mojombo.com>
+committer-time 1192860483
+committer-tz -0700
+filename lib/grit.rb
+summary implement Repo.init_bare
+ class << self
+b6e1b765e0c15586a2c5b9832854f95defd71e1f 19 19
+ attr_accessor :debug
+b6e1b765e0c15586a2c5b9832854f95defd71e1f 20 20
+ end
+b6e1b765e0c15586a2c5b9832854f95defd71e1f 21 21
+
+b6e1b765e0c15586a2c5b9832854f95defd71e1f 22 22
+ self.debug = false
+b6e1b765e0c15586a2c5b9832854f95defd71e1f 23 23
+
+634396b2f541a9f2d58b00be1a07f0c358b999b3 11 24 2
+ VERSION = '1.0.0'
+634396b2f541a9f2d58b00be1a07f0c358b999b3 12 25
+ end \ No newline at end of file
diff --git a/test/fixtures/cat_file_blob b/test/fixtures/cat_file_blob
new file mode 100644
index 00000000..70c379b6
--- /dev/null
+++ b/test/fixtures/cat_file_blob
@@ -0,0 +1 @@
+Hello world \ No newline at end of file
diff --git a/test/fixtures/cat_file_blob_size b/test/fixtures/cat_file_blob_size
new file mode 100644
index 00000000..b4de3947
--- /dev/null
+++ b/test/fixtures/cat_file_blob_size
@@ -0,0 +1 @@
+11
diff --git a/test/fixtures/diff_2 b/test/fixtures/diff_2
new file mode 100644
index 00000000..1f060c70
--- /dev/null
+++ b/test/fixtures/diff_2
@@ -0,0 +1,54 @@
+diff --git a/lib/grit/commit.rb b/lib/grit/commit.rb
+index a093bb1db8e884cccf396b297259181d1caebed4..80fd3d527f269ecbd570b65b8e21fd85baedb6e9 100644
+--- a/lib/grit/commit.rb
++++ b/lib/grit/commit.rb
+@@ -156,12 +156,8 @@ module Grit
+
+ def diffs
+ if parents.empty?
+- diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id)
+- if diff =~ /diff --git a/
+- diff = diff.sub(/.+?(diff --git a)/m, '\1')
+- else
+- diff = ''
+- end
++ diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id)
++ diff = diff.sub(/.+?(diff --git a)/m, '\1')
+ Diff.list_from_string(@repo, diff)
+ else
+ self.class.diff(@repo, parents.first.id, @id)
+diff --git a/test/fixtures/show_empty_commit b/test/fixtures/show_empty_commit
+deleted file mode 100644
+index ea25e32a409fdf74c1b9268820108d1c16dcc553..0000000000000000000000000000000000000000
+--- a/test/fixtures/show_empty_commit
++++ /dev/null
+@@ -1,6 +0,0 @@
+-commit 1e3824339762bd48316fe87bfafc853732d43264
+-tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+-author Tom Preston-Werner <tom@mojombo.com> 1157392833 +0000
+-committer Tom Preston-Werner <tom@mojombo.com> 1157392833 +0000
+-
+- initial directory structure
+diff --git a/test/test_commit.rb b/test/test_commit.rb
+index fdeb9000089b052f0b31a845e0173e9b089e06a0..bdbc450e08084d7d611e985cfa12fb424cab29b2 100644
+--- a/test/test_commit.rb
++++ b/test/test_commit.rb
+@@ -98,18 +98,6 @@ class TestCommit < Test::Unit::TestCase
+ assert_equal true, diffs[5].new_file
+ end
+
+- def test_diffs_on_initial_import_with_empty_commit
+- Git.any_instance.expects(:show).with(
+- {:full_index => true, :pretty => 'raw'},
+- '634396b2f541a9f2d58b00be1a07f0c358b999b3'
+- ).returns(fixture('show_empty_commit'))
+-
+- @c = Commit.create(@r, :id => '634396b2f541a9f2d58b00be1a07f0c358b999b3')
+- diffs = @c.diffs
+-
+- assert_equal [], diffs
+- end
+-
+ # to_s
+
+ def test_to_s
diff --git a/test/fixtures/diff_2f b/test/fixtures/diff_2f
new file mode 100644
index 00000000..5246cd6b
--- /dev/null
+++ b/test/fixtures/diff_2f
@@ -0,0 +1,19 @@
+diff --git a/lib/grit/commit.rb b/lib/grit/commit.rb
+index a093bb1db8e884cccf396b297259181d1caebed4..80fd3d527f269ecbd570b65b8e21fd85baedb6e9 100644
+--- a/lib/grit/commit.rb
++++ b/lib/grit/commit.rb
+@@ -156,12 +156,8 @@ module Grit
+
+ def diffs
+ if parents.empty?
+- diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id)
+- if diff =~ /diff --git a/
+- diff = diff.sub(/.+?(diff --git a)/m, '\1')
+- else
+- diff = ''
+- end
++ diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id)
++ diff = diff.sub(/.+?(diff --git a)/m, '\1')
+ Diff.list_from_string(@repo, diff)
+ else
+ self.class.diff(@repo, parents.first.id, @id)
diff --git a/test/fixtures/diff_f b/test/fixtures/diff_f
new file mode 100644
index 00000000..48a49256
--- /dev/null
+++ b/test/fixtures/diff_f
@@ -0,0 +1,15 @@
+diff --git a/lib/grit/diff.rb b/lib/grit/diff.rb
+index 537955bb86a8ceaa19aea89e75ccbea5ce6f2698..00b0b4a67eca9242db5f8991e99625acd55f040c 100644
+--- a/lib/grit/diff.rb
++++ b/lib/grit/diff.rb
+@@ -27,6 +27,10 @@ module Grit
+ while !lines.empty?
+ m, a_path, b_path = *lines.shift.match(%r{^diff --git a/(\S+) b/(\S+)$})
+
++ if lines.first =~ /^old mode/
++ 2.times { lines.shift }
++ end
++
+ new_file = false
+ deleted_file = false
+
diff --git a/test/fixtures/diff_i b/test/fixtures/diff_i
new file mode 100644
index 00000000..cec64e1d
--- /dev/null
+++ b/test/fixtures/diff_i
@@ -0,0 +1,201 @@
+commit 634396b2f541a9f2d58b00be1a07f0c358b999b3
+Author: Tom Preston-Werner <tom@mojombo.com>
+Date: Tue Oct 9 23:18:20 2007 -0700
+
+ initial grit setup
+
+diff --git a/History.txt b/History.txt
+new file mode 100644
+index 0000000000000000000000000000000000000000..81d2c27608b352814cbe979a6acd678d30219678
+--- /dev/null
++++ b/History.txt
+@@ -0,0 +1,5 @@
++== 1.0.0 / 2007-10-09
++
++* 1 major enhancement
++ * Birthday!
++
+diff --git a/Manifest.txt b/Manifest.txt
+new file mode 100644
+index 0000000000000000000000000000000000000000..641972d82c6d1b51122274ae8f6a0ecdfb56ee22
+--- /dev/null
++++ b/Manifest.txt
+@@ -0,0 +1,7 @@
++History.txt
++Manifest.txt
++README.txt
++Rakefile
++bin/grit
++lib/grit.rb
++test/test_grit.rb
+\ No newline at end of file
+diff --git a/README.txt b/README.txt
+new file mode 100644
+index 0000000000000000000000000000000000000000..8b1e02c0fb554eed2ce2ef737a68bb369d7527df
+--- /dev/null
++++ b/README.txt
+@@ -0,0 +1,48 @@
++grit
++ by FIX (your name)
++ FIX (url)
++
++== DESCRIPTION:
++
++FIX (describe your package)
++
++== FEATURES/PROBLEMS:
++
++* FIX (list of features or problems)
++
++== SYNOPSIS:
++
++ FIX (code sample of usage)
++
++== REQUIREMENTS:
++
++* FIX (list of requirements)
++
++== INSTALL:
++
++* FIX (sudo gem install, anything else)
++
++== LICENSE:
++
++(The MIT License)
++
++Copyright (c) 2007 FIX
++
++Permission is hereby granted, free of charge, to any person obtaining
++a copy of this software and associated documentation files (the
++'Software'), to deal in the Software without restriction, including
++without limitation the rights to use, copy, modify, merge, publish,
++distribute, sublicense, and/or sell copies of the Software, and to
++permit persons to whom the Software is furnished to do so, subject to
++the following conditions:
++
++The above copyright notice and this permission notice shall be
++included in all copies or substantial portions of the Software.
++
++THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
++EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
++MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
++IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
++CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
++TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
++SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+diff --git a/Rakefile b/Rakefile
+new file mode 100644
+index 0000000000000000000000000000000000000000..ff69c3684a18592c741332b290492aa39d980e02
+--- /dev/null
++++ b/Rakefile
+@@ -0,0 +1,17 @@
++# -*- ruby -*-
++
++require 'rubygems'
++require 'hoe'
++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.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
++end
++
++# vim: syntax=Ruby
+diff --git a/bin/grit b/bin/grit
+new file mode 100644
+index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+diff --git a/lib/grit.rb b/lib/grit.rb
+new file mode 100644
+index 0000000000000000000000000000000000000000..32cec87d1e78946a827ddf6a8776be4d81dcf1d1
+--- /dev/null
++++ b/lib/grit.rb
+@@ -0,0 +1,12 @@
++$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
++
++# core
++
++# stdlib
++
++# internal requires
++require 'grit/grit'
++
++class Grit
++ VERSION = '1.0.0'
++end
+\ No newline at end of file
+diff --git a/lib/grit/errors.rb b/lib/grit/errors.rb
+new file mode 100644
+index 0000000000000000000000000000000000000000..b3be31553741937607a89be8b6a2ab1df208852e
+--- /dev/null
++++ b/lib/grit/errors.rb
+@@ -0,0 +1,4 @@
++class Grit
++ class InvalidGitRepositoryError < StandardError
++ end
++end
+\ No newline at end of file
+diff --git a/lib/grit/grit.rb b/lib/grit/grit.rb
+new file mode 100644
+index 0000000000000000000000000000000000000000..48fd36e16081ec09903f7a0e2253b3d16f9efb01
+--- /dev/null
++++ b/lib/grit/grit.rb
+@@ -0,0 +1,24 @@
++class Grit
++ attr_accessor :path
++
++ # Create a new Grit instance
++ # +path+ is the path to either the root git directory or the bare git repo
++ #
++ # Examples
++ # g = Grit.new("/Users/tom/dev/grit")
++ # g = Grit.new("/Users/tom/public/grit.git")
++ def initialize(path)
++ if File.exist?(File.join(path, '.git'))
++ self.path = File.join(path, '.git')
++ elsif File.exist?(path) && path =~ /\.git$/
++ self.path = path
++ else
++ raise InvalidGitRepositoryError.new(path) unless File.exist?(path)
++ end
++ end
++
++ # Return the project's description. Taken verbatim from REPO/description
++ def description
++ File.open(File.join(self.path, 'description')).read.chomp
++ end
++end
+\ No newline at end of file
+diff --git a/test/helper.rb b/test/helper.rb
+new file mode 100644
+index 0000000000000000000000000000000000000000..56e21da6b4ce3021d2754775dfa589947a4e37e5
+--- /dev/null
++++ b/test/helper.rb
+@@ -0,0 +1,5 @@
++require File.join(File.dirname(__FILE__), *%w[.. lib grit])
++
++require 'test/unit'
++
++GRIT_REPO = File.join(File.dirname(__FILE__), *%w[..])
+diff --git a/test/test_grit.rb b/test/test_grit.rb
+new file mode 100644
+index 0000000000000000000000000000000000000000..93aa481b37629797df739380306ae689e13f2855
+--- /dev/null
++++ b/test/test_grit.rb
+@@ -0,0 +1,11 @@
++require File.dirname(__FILE__) + '/helper'
++
++class TestGrit < Test::Unit::TestCase
++ def setup
++ @g = Grit.new(GRIT_REPO)
++ end
++
++ def test_description
++ assert_equal "Grit is a ruby library for interfacing with git repositories.", @g.description
++ end
++end
+\ No newline at end of file
diff --git a/test/fixtures/diff_mode_only b/test/fixtures/diff_mode_only
new file mode 100644
index 00000000..6fc18f69
--- /dev/null
+++ b/test/fixtures/diff_mode_only
@@ -0,0 +1,1152 @@
+diff --git a/bin/merb b/bin/merb
+old mode 100644
+new mode 100755
+diff --git a/lib/merb.rb b/lib/merb.rb
+index 76cb3e269e46fdf9b63cda7cb563c6cf40fdcb15..a2ab4ed47f9cb2ab942da5c46a2b561758a0d704 100644
+--- a/lib/merb.rb
++++ b/lib/merb.rb
+@@ -15,7 +15,7 @@ require 'merb_core/core_ext'
+ require 'merb_core/gem_ext/erubis'
+ require 'merb_core/logger'
+ require 'merb_core/version'
+-
++require 'merb_core/controller/mime'
+
+ module Merb
+ class << self
+@@ -23,6 +23,7 @@ module Merb
+ def start(argv=ARGV)
+ Merb::Config.parse_args(argv)
+ BootLoader.run
++
+ case Merb::Config[:adapter]
+ when "mongrel"
+ adapter = Merb::Rack::Mongrel
+diff --git a/lib/merb_core/boot/bootloader.rb b/lib/merb_core/boot/bootloader.rb
+index d873924860bf4da06ac93db5c6a188f63dd1c3cc..57da75f05e28e8a256922bf345ccd3902e0a0b02 100644
+--- a/lib/merb_core/boot/bootloader.rb
++++ b/lib/merb_core/boot/bootloader.rb
+@@ -20,7 +20,7 @@ module Merb
+ end
+
+ def run
+- subclasses.each {|klass| Object.full_const_get(klass).new.run }
++ subclasses.each {|klass| Object.full_const_get(klass).run }
+ end
+
+ def after(klass)
+@@ -37,95 +37,128 @@ module Merb
+
+ end
+
+-class Merb::BootLoader::BuildFramework < Merb::BootLoader
+- def run
+- build_framework
++class Merb::BootLoader::LoadInit < Merb::BootLoader
++ def self.run
++ if Merb::Config[:init_file]
++ require Merb.root / Merb::Config[:init_file]
++ elsif File.exists?(Merb.root / "config" / "merb_init.rb")
++ require Merb.root / "config" / "merb_init"
++ elsif File.exists?(Merb.root / "merb_init.rb")
++ require Merb.root / "merb_init"
++ elsif File.exists?(Merb.root / "application.rb")
++ require Merb.root / "application"
++ end
++ end
++end
++
++class Merb::BootLoader::Environment < Merb::BootLoader
++ def self.run
++ Merb.environment = Merb::Config[:environment]
++ end
++end
++
++class Merb::BootLoader::Logger < Merb::BootLoader
++ def self.run
++ Merb.logger = Merb::Logger.new(Merb.dir_for(:log) / "test_log")
++ Merb.logger.level = Merb::Logger.const_get(Merb::Config[:log_level].upcase) rescue Merb::Logger::INFO
+ end
++end
++
++class Merb::BootLoader::BuildFramework < Merb::BootLoader
++ class << self
++ def run
++ build_framework
++ end
+
+- # This method should be overridden in merb_init.rb before Merb.start to set up a different
+- # framework structure
+- def build_framework
+- %[view model controller helper mailer part].each do |component|
+- Merb.push_path(component.to_sym, Merb.root_path("app/#{component}s"))
++ # This method should be overridden in merb_init.rb before Merb.start to set up a different
++ # framework structure
++ def build_framework
++ %w[view model controller helper mailer part].each do |component|
++ Merb.push_path(component.to_sym, Merb.root_path("app/#{component}s"))
++ end
++ Merb.push_path(:application, Merb.root_path("app/controllers/application.rb"))
++ Merb.push_path(:config, Merb.root_path("config/router.rb"))
++ Merb.push_path(:lib, Merb.root_path("lib"))
+ end
+- Merb.push_path(:application, Merb.root_path("app/controllers/application.rb"))
+- Merb.push_path(:config, Merb.root_path("config/router.rb"))
+- Merb.push_path(:lib, Merb.root_path("lib"))
+ end
+ end
+
+ class Merb::BootLoader::LoadPaths < Merb::BootLoader
+ LOADED_CLASSES = {}
+
+- def run
+- # Add models, controllers, and lib to the load path
+- $LOAD_PATH.unshift Merb.load_paths[:model].first if Merb.load_paths[:model]
+- $LOAD_PATH.unshift Merb.load_paths[:controller].first if Merb.load_paths[:controller]
+- $LOAD_PATH.unshift Merb.load_paths[:lib].first if Merb.load_paths[:lib]
++ class << self
++ def run
++ # Add models, controllers, and lib to the load path
++ $LOAD_PATH.unshift Merb.load_paths[:model].first if Merb.load_paths[:model]
++ $LOAD_PATH.unshift Merb.load_paths[:controller].first if Merb.load_paths[:controller]
++ $LOAD_PATH.unshift Merb.load_paths[:lib].first if Merb.load_paths[:lib]
+
+- # Require all the files in the registered load paths
+- puts Merb.load_paths.inspect
+- Merb.load_paths.each do |name, path|
+- Dir[path.first / path.last].each do |file|
+- klasses = ObjectSpace.classes.dup
+- require f
+- LOADED_CLASSES[file] = ObjectSpace.classes - klasses
++ # Require all the files in the registered load paths
++ puts Merb.load_paths.inspect
++ Merb.load_paths.each do |name, path|
++ Dir[path.first / path.last].each do |file|
++ klasses = ObjectSpace.classes.dup
++ require file
++ LOADED_CLASSES[file] = ObjectSpace.classes - klasses
++ end
+ end
+ end
+- end
+
+- def reload(file)
+- if klasses = LOADED_CLASSES[file]
+- klasses.each do |klass|
+- remove_constant(klass)
++ def reload(file)
++ if klasses = LOADED_CLASSES[file]
++ klasses.each do |klass|
++ remove_constant(klass)
++ end
+ end
++ load file
+ end
+- load file
+- end
+
+- def remove_constant(const)
+- # This is to support superclasses (like AbstractController) that track
+- # their subclasses in a class variable. Classes that wish to use this
+- # functionality are required to alias it to _subclasses_list. Plugins
+- # for ORMs and other libraries should keep this in mind.
+- if klass.superclass.respond_to?(:_subclasses_list)
+- klass.superclass.send(:_subclasses_list).delete(klass)
+- klass.superclass.send(:_subclasses_list).delete(klass.to_s)
+- end
++ def remove_constant(const)
++ # This is to support superclasses (like AbstractController) that track
++ # their subclasses in a class variable. Classes that wish to use this
++ # functionality are required to alias it to _subclasses_list. Plugins
++ # for ORMs and other libraries should keep this in mind.
++ if klass.superclass.respond_to?(:_subclasses_list)
++ klass.superclass.send(:_subclasses_list).delete(klass)
++ klass.superclass.send(:_subclasses_list).delete(klass.to_s)
++ end
+
+- parts = const.to_s.split("::")
+- base = parts.size == 1 ? Object : Object.full_const_get(parts[0..-2].join("::"))
+- object = parts[-1].intern
+- Merb.logger.debugger("Removing constant #{object} from #{base}")
+- base.send(:remove_const, object) if object
++ parts = const.to_s.split("::")
++ base = parts.size == 1 ? Object : Object.full_const_get(parts[0..-2].join("::"))
++ object = parts[-1].intern
++ Merb.logger.debugger("Removing constant #{object} from #{base}")
++ base.send(:remove_const, object) if object
++ end
+ end
+
+ end
+
+ class Merb::BootLoader::Templates < Merb::BootLoader
+- def run
+- template_paths.each do |path|
+- Merb::Template.inline_template(path)
++ class << self
++ def run
++ template_paths.each do |path|
++ Merb::Template.inline_template(path)
++ end
+ end
+- end
+
+- def template_paths
+- extension_glob = "{#{Merb::Template::EXTENSIONS.keys.join(',')}}"
++ def template_paths
++ extension_glob = "{#{Merb::Template::EXTENSIONS.keys.join(',')}}"
+
+- # This gets all templates set in the controllers template roots
+- # We separate the two maps because most of controllers will have
+- # the same _template_root, so it's silly to be globbing the same
+- # path over and over.
+- template_paths = Merb::AbstractController._abstract_subclasses.map do |klass|
+- Object.full_const_get(klass)._template_root
+- end.uniq.map {|path| Dir["#{path}/**/*.#{extension_glob}"] }
++ # This gets all templates set in the controllers template roots
++ # We separate the two maps because most of controllers will have
++ # the same _template_root, so it's silly to be globbing the same
++ # path over and over.
++ template_paths = Merb::AbstractController._abstract_subclasses.map do |klass|
++ Object.full_const_get(klass)._template_root
++ end.uniq.compact.map {|path| Dir["#{path}/**/*.#{extension_glob}"] }
+
+- # This gets the templates that might be created outside controllers
+- # template roots. eg app/views/shared/*
+- template_paths << Dir["#{Merb.dir_for(:view)}/**/*.#{extension_glob}"] if Merb.dir_for(:view)
++ # This gets the templates that might be created outside controllers
++ # template roots. eg app/views/shared/*
++ template_paths << Dir["#{Merb.dir_for(:view)}/**/*.#{extension_glob}"] if Merb.dir_for(:view)
+
+- template_paths.flatten.compact.uniq
+- end
++ template_paths.flatten.compact.uniq
++ end
++ end
+ end
+
+ class Merb::BootLoader::Libraries < Merb::BootLoader
+@@ -145,18 +178,41 @@ class Merb::BootLoader::Libraries < Merb::BootLoader
+ def self.add_libraries(hsh)
+ @@libraries.merge!(hsh)
+ end
+-
+- def run
++
++ def self.run
+ @@libraries.each do |exclude, choices|
+ require_first_working(*choices) unless Merb::Config[exclude]
+ end
+ end
+-
+- def require_first_working(first, *rest)
++
++ def self.require_first_working(first, *rest)
+ p first, rest
+ require first
+ rescue LoadError
+ raise LoadError if rest.empty?
+ require_first_working rest.unshift, *rest
+ end
++end
++
++class Merb::BootLoader::MimeTypes < Merb::BootLoader
++ def self.run
++ # Sets the default mime-types
++ #
++ # By default, the mime-types include:
++ # :all:: no transform, */*
++ # :yaml:: to_yaml, application/x-yaml or text/yaml
++ # :text:: to_text, text/plain
++ # :html:: to_html, text/html or application/xhtml+xml or application/html
++ # :xml:: to_xml, application/xml or text/xml or application/x-xml, adds "Encoding: UTF-8" response header
++ # :js:: to_json, text/javascript ot application/javascript or application/x-javascript
++ # :json:: to_json, application/json or text/x-json
++ Merb.available_mime_types.clear
++ Merb.add_mime_type(:all, nil, %w[*/*])
++ Merb.add_mime_type(:yaml, :to_yaml, %w[application/x-yaml text/yaml])
++ Merb.add_mime_type(:text, :to_text, %w[text/plain])
++ Merb.add_mime_type(:html, :to_html, %w[text/html application/xhtml+xml application/html])
++ Merb.add_mime_type(:xml, :to_xml, %w[application/xml text/xml application/x-xml], :Encoding => "UTF-8")
++ Merb.add_mime_type(:js, :to_json, %w[text/javascript application/javascript application/x-javascript])
++ Merb.add_mime_type(:json, :to_json, %w[application/json text/x-json])
++ end
+ end
+\ No newline at end of file
+diff --git a/lib/merb_core/config.rb b/lib/merb_core/config.rb
+index c92f2e6f071c234551ecb16a4716d47fa92f6c7b..ab0864e0174b54833c758f9f22a840d3b53c7653 100644
+--- a/lib/merb_core/config.rb
++++ b/lib/merb_core/config.rb
+@@ -92,6 +92,10 @@ module Merb
+ options[:cluster] = nodes
+ end
+
++ opts.on("-I", "--init-file FILE", "Name of the file to load first") do |init_file|
++ options[:init_file] = init_file
++ end
++
+ opts.on("-p", "--port PORTNUM", "Port to run merb on, defaults to 4000.") do |port|
+ options[:port] = port
+ end
+@@ -261,29 +265,29 @@ module Merb
+
+ @configuration = Merb::Config.apply_configuration_from_file options, environment_merb_yml
+
+- case Merb::Config[:environment].to_s
+- when 'production'
+- Merb::Config[:reloader] = Merb::Config.fetch(:reloader, false)
+- Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, false)
+- Merb::Config[:cache_templates] = true
+- else
+- Merb::Config[:reloader] = Merb::Config.fetch(:reloader, true)
+- Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, true)
+- end
+-
+- Merb::Config[:reloader_time] ||= 0.5 if Merb::Config[:reloader] == true
+-
+-
+- if Merb::Config[:reloader]
+- Thread.abort_on_exception = true
+- Thread.new do
+- loop do
+- sleep( Merb::Config[:reloader_time] )
+- ::Merb::BootLoader.reload if ::Merb::BootLoader.app_loaded?
+- end
+- Thread.exit
+- end
+- end
++ # case Merb::Config[:environment].to_s
++ # when 'production'
++ # Merb::Config[:reloader] = Merb::Config.fetch(:reloader, false)
++ # Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, false)
++ # Merb::Config[:cache_templates] = true
++ # else
++ # Merb::Config[:reloader] = Merb::Config.fetch(:reloader, true)
++ # Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, true)
++ # end
++ #
++ # Merb::Config[:reloader_time] ||= 0.5 if Merb::Config[:reloader] == true
++ #
++ #
++ # if Merb::Config[:reloader]
++ # Thread.abort_on_exception = true
++ # Thread.new do
++ # loop do
++ # sleep( Merb::Config[:reloader_time] )
++ # ::Merb::BootLoader.reload if ::Merb::BootLoader.app_loaded?
++ # end
++ # Thread.exit
++ # end
++ # end
+ @configuration
+ end
+
+diff --git a/lib/merb_core/controller/abstract_controller.rb b/lib/merb_core/controller/abstract_controller.rb
+index fbf83372793da6da4b803b799994f0e341fddf88..f5e9a59057d67a6d56377a516a726cf51aa03d6f 100644
+--- a/lib/merb_core/controller/abstract_controller.rb
++++ b/lib/merb_core/controller/abstract_controller.rb
+@@ -96,7 +96,7 @@ class Merb::AbstractController
+ # the superclass.
+ #---
+ # @public
+- def _template_location(action, controller = controller_name, type = nil)
++ def _template_location(action, type = nil, controller = controller_name)
+ "#{controller}/#{action}"
+ end
+
+@@ -106,6 +106,8 @@ class Merb::AbstractController
+ # own subclasses. We're using a Set so we don't have to worry about
+ # uniqueness.
+ self._abstract_subclasses = Set.new
++ self._template_root = Merb.dir_for(:view)
++
+ def self.subclasses_list() _abstract_subclasses end
+
+ class << self
+@@ -114,7 +116,6 @@ class Merb::AbstractController
+ # The controller that is being inherited from Merb::AbstractController
+ def inherited(klass)
+ _abstract_subclasses << klass.to_s
+- klass._template_root ||= Merb.dir_for(:view)
+ super
+ end
+
+diff --git a/lib/merb_core/controller/merb_controller.rb b/lib/merb_core/controller/merb_controller.rb
+index 7283f006bb0501b29f825da129600cf045264b62..98af6ef3330a6b3f46d7bb1f8643261e28155ae5 100644
+--- a/lib/merb_core/controller/merb_controller.rb
++++ b/lib/merb_core/controller/merb_controller.rb
+@@ -71,6 +71,10 @@ class Merb::Controller < Merb::AbstractController
+ end
+ end
+
++ def _template_location(action, type = nil, controller = controller_name)
++ "#{controller}/#{action}.#{type}"
++ end
++
+ # Sets the variables that came in through the dispatch as available to
+ # the controller. This is called by .build, so see it for more
+ # information.
+@@ -107,9 +111,7 @@ class Merb::Controller < Merb::AbstractController
+ request.cookies[_session_id_key] = request.params[_session_id_key]
+ end
+ end
+- @_request, @_response, @_status, @_headers =
+- request, response, status, headers
+-
++ @request, @response, @status, @headers = request, response, status, headers
+ nil
+ end
+
+@@ -135,7 +137,8 @@ class Merb::Controller < Merb::AbstractController
+ @_benchmarks[:action_time] = Time.now - start
+ end
+
+- _attr_reader :request, :response, :status, :headers
++ attr_reader :request, :response, :headers
++ attr_accessor :status
+ def params() request.params end
+ def cookies() request.cookies end
+ def session() request.session end
+diff --git a/lib/merb_core/controller/mime.rb b/lib/merb_core/controller/mime.rb
+index d17570786ca318cff7201c4b1e947ae229b01de8..ff9abe4d1c452aeabfcf5f7dc7a2c7cdd3f67035 100644
+--- a/lib/merb_core/controller/mime.rb
++++ b/lib/merb_core/controller/mime.rb
+@@ -8,7 +8,7 @@ module Merb
+
+ # Any specific outgoing headers should be included here. These are not
+ # the content-type header but anything in addition to it.
+- # +tranform_method+ should be set to a symbol of the method used to
++ # +transform_method+ should be set to a symbol of the method used to
+ # transform a resource into this mime type.
+ # For example for the :xml mime type an object might be transformed by
+ # calling :to_xml, or for the :js mime type, :to_json.
+@@ -71,27 +71,6 @@ module Merb
+ def mime_by_request_header(header)
+ available_mime_types.find {|key,info| info[request_headers].include?(header)}.first
+ end
+-
+- # Resets the default mime-types
+- #
+- # By default, the mime-types include:
+- # :all:: no transform, */*
+- # :yaml:: to_yaml, application/x-yaml or text/yaml
+- # :text:: to_text, text/plain
+- # :html:: to_html, text/html or application/xhtml+xml or application/html
+- # :xml:: to_xml, application/xml or text/xml or application/x-xml, adds "Encoding: UTF-8" response header
+- # :js:: to_json, text/javascript ot application/javascript or application/x-javascript
+- # :json:: to_json, application/json or text/x-json
+- def reset_default_mime_types!
+- available_mime_types.clear
+- Merb.add_mime_type(:all, nil, %w[*/*])
+- Merb.add_mime_type(:yaml, :to_yaml, %w[application/x-yaml text/yaml])
+- Merb.add_mime_type(:text, :to_text, %w[text/plain])
+- Merb.add_mime_type(:html, :to_html, %w[text/html application/xhtml+xml application/html])
+- Merb.add_mime_type(:xml, :to_xml, %w[application/xml text/xml application/x-xml], :Encoding => "UTF-8")
+- Merb.add_mime_type(:js, :to_json, %w[text/javascript application/javascript application/x-javascript])
+- Merb.add_mime_type(:json, :to_json, %w[application/json text/x-json])
+- end
+
+ end
+ end
+\ No newline at end of file
+diff --git a/lib/merb_core/controller/mixins/render.rb b/lib/merb_core/controller/mixins/render.rb
+index 8e096546d4647bb597ab2e00a4b15d09db35e9c9..a298263af7d655d9ce43007554f3827046831287 100644
+--- a/lib/merb_core/controller/mixins/render.rb
++++ b/lib/merb_core/controller/mixins/render.rb
+@@ -51,21 +51,22 @@ module Merb::RenderMixin
+
+ # If you don't specify a thing to render, assume they want to render the current action
+ thing ||= action_name.to_sym
+-
++
+ # Content negotiation
+ opts[:format] ? (self.content_type = opts[:format]) : content_type
+
+ # Do we have a template to try to render?
+ if thing.is_a?(Symbol) || opts[:template]
+-
++
+ # Find a template path to look up (_template_location adds flexibility here)
+- template_location = _template_root / (opts[:template] || _template_location(thing))
++ template_location = _template_root / (opts[:template] || _template_location(thing, content_type))
++
+ # Get the method name from the previously inlined list
+ template_method = Merb::Template.template_for(template_location)
+
+ # Raise an error if there's no template
+ raise TemplateNotFound, "No template found at #{template_location}" unless
+- self.respond_to?(template_method)
++ template_method && self.respond_to?(template_method)
+
+ # Call the method in question and throw the content for later consumption by the layout
+ throw_content(:for_layout, self.send(template_method))
+diff --git a/lib/merb_core/controller/mixins/responder.rb b/lib/merb_core/controller/mixins/responder.rb
+index e910b2b32c844ab51cf2a10d0ad26c314dbb3631..5ac67fb907aaf9f95effc7eb3cbb07b8963ce022 100644
+--- a/lib/merb_core/controller/mixins/responder.rb
++++ b/lib/merb_core/controller/mixins/responder.rb
+@@ -97,6 +97,8 @@ module Merb
+ # and none of the provides methods can be used.
+ module ResponderMixin
+
++ TYPES = {}
++
+ class ContentTypeAlreadySet < StandardError; end
+
+ # ==== Parameters
+@@ -105,6 +107,7 @@ module Merb
+ base.extend(ClassMethods)
+ base.class_eval do
+ class_inheritable_accessor :class_provided_formats
++ self.class_provided_formats = []
+ end
+ base.reset_provides
+ end
+@@ -178,171 +181,253 @@ module Merb
+ def reset_provides
+ only_provides(:html)
+ end
+-
+- # ==== Returns
+- # The current list of formats provided for this instance of the controller.
+- # It starts with what has been set in the controller (or :html by default)
+- # but can be modifed on a per-action basis.
+- def _provided_formats
+- @_provided_formats ||= class_provided_formats.dup
++ end
++
++ # ==== Returns
++ # The current list of formats provided for this instance of the controller.
++ # It starts with what has been set in the controller (or :html by default)
++ # but can be modifed on a per-action basis.
++ def _provided_formats
++ @_provided_formats ||= class_provided_formats.dup
++ end
++
++ # Sets the provided formats for this action. Usually, you would
++ # use a combination of +provides+, +only_provides+ and +does_not_provide+
++ # to manage this, but you can set it directly.
++ #
++ # ==== Parameters
++ # *formats<Symbol>:: A list of formats to be passed to provides
++ #
++ # ==== Raises
++ # Merb::ResponderMixin::ContentTypeAlreadySet::
++ # Content negotiation already occured, and the content_type is set.
++ #
++ # ==== Returns
++ # Array:: List of formats passed in
++ def _set_provided_formats(*formats)
++ if @_content_type
++ raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set"
+ end
+-
+- # Sets the provided formats for this action. Usually, you would
+- # use a combination of +provides+, +only_provides+ and +does_not_provide+
+- # to manage this, but you can set it directly.
+- #
+- # ==== Parameters
+- # *formats<Symbol>:: A list of formats to be passed to provides
+- #
+- # ==== Raises
+- # Merb::ResponderMixin::ContentTypeAlreadySet::
+- # Content negotiation already occured, and the content_type is set.
+- #
+- # ==== Returns
+- # Array:: List of formats passed in
+- def _set_provided_formats(*formats)
+- if @_content_type
+- raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set"
+- end
+- @_provided_formats = []
+- provides(*formats)
++ @_provided_formats = []
++ provides(*formats)
++ end
++ alias :_provided_formats= :_set_provided_formats
++
++ # Adds formats to the list of provided formats for this particular
++ # request. Usually used to add formats to a single action. See also
++ # the controller-level provides that affects all actions in a controller.
++ #
++ # ==== Parameters
++ # *formats<Symbol>:: A list of formats to add to the per-action list
++ # of provided formats
++ #
++ # ==== Raises
++ # Merb::ResponderMixin::ContentTypeAlreadySet::
++ # Content negotiation already occured, and the content_type is set.
++ #
++ # ==== Returns
++ # Array:: List of formats passed in
++ #
++ #---
++ # @public
++ def provides(*formats)
++ if @_content_type
++ raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set"
+ end
+- alias :_provided_formats= :_set_provided_formats
+-
+- # Adds formats to the list of provided formats for this particular
+- # request. Usually used to add formats to a single action. See also
+- # the controller-level provides that affects all actions in a controller.
+- #
+- # ==== Parameters
+- # *formats<Symbol>:: A list of formats to add to the per-action list
+- # of provided formats
+- #
+- # ==== Raises
+- # Merb::ResponderMixin::ContentTypeAlreadySet::
+- # Content negotiation already occured, and the content_type is set.
+- #
+- # ==== Returns
+- # Array:: List of formats passed in
+- #
+- #---
+- # @public
+- def provides(*formats)
+- if @_content_type
+- raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set"
+- end
+- formats.each do |fmt|
+- _provided_formats << fmt unless _provided_formats.include?(fmt)
+- end
++ formats.each do |fmt|
++ _provided_formats << fmt unless _provided_formats.include?(fmt)
+ end
++ end
+
+- # Sets list of provided formats for this particular
+- # request. Usually used to limit formats to a single action. See also
+- # the controller-level only_provides that affects all actions
+- # in a controller.
+- #
+- # ==== Parameters
+- # *formats<Symbol>:: A list of formats to use as the per-action list
+- # of provided formats
+- #
+- # ==== Returns
+- # Array:: List of formats passed in
+- #
+- #---
+- # @public
+- def only_provides(*formats)
+- self._provided_formats = *formats
+- end
+-
+- # Removes formats from the list of provided formats for this particular
+- # request. Usually used to remove formats from a single action. See
+- # also the controller-level does_not_provide that affects all actions in a
+- # controller.
+- #
+- # ==== Parameters
+- # *formats<Symbol>:: Registered mime-type
+- #
+- # ==== Returns
+- # Array:: List of formats that remain after removing the ones not to provide
+- #
+- #---
+- # @public
+- def does_not_provide(*formats)
+- formats.flatten!
+- self._provided_formats -= formats
+- end
+-
+- # Do the content negotiation:
+- # 1. if params[:format] is there, and provided, use it
+- # 2. Parse the Accept header
+- # 3. If it's */*, use the first provided format
+- # 4. Look for one that is provided, in order of request
+- # 5. Raise 406 if none found
+- def _perform_content_negotiation # :nodoc:
+- raise Merb::ControllerExceptions::NotAcceptable if provided_formats.empty?
+- if fmt = params[:format]
+- return fmt.to_sym if provided_formats.include?(fmt.to_sym)
+- else
+- accepts = Responder.parse(request.accept).map {|t| t.to_sym}
+- return provided_formats.first if accepts.include?(:all)
+- return accepts.each { |type| break type if provided_formats.include?(type) }
+- end
+- raise Merb::ControllerExceptions::NotAcceptable
++ # Sets list of provided formats for this particular
++ # request. Usually used to limit formats to a single action. See also
++ # the controller-level only_provides that affects all actions
++ # in a controller.
++ #
++ # ==== Parameters
++ # *formats<Symbol>:: A list of formats to use as the per-action list
++ # of provided formats
++ #
++ # ==== Returns
++ # Array:: List of formats passed in
++ #
++ #---
++ # @public
++ def only_provides(*formats)
++ self._provided_formats = *formats
++ end
++
++ # Removes formats from the list of provided formats for this particular
++ # request. Usually used to remove formats from a single action. See
++ # also the controller-level does_not_provide that affects all actions in a
++ # controller.
++ #
++ # ==== Parameters
++ # *formats<Symbol>:: Registered mime-type
++ #
++ # ==== Returns
++ # Array:: List of formats that remain after removing the ones not to provide
++ #
++ #---
++ # @public
++ def does_not_provide(*formats)
++ formats.flatten!
++ self._provided_formats -= formats
++ end
++
++ # Do the content negotiation:
++ # 1. if params[:format] is there, and provided, use it
++ # 2. Parse the Accept header
++ # 3. If it's */*, use the first provided format
++ # 4. Look for one that is provided, in order of request
++ # 5. Raise 406 if none found
++ def _perform_content_negotiation # :nodoc:
++ raise Merb::ControllerExceptions::NotAcceptable if _provided_formats.empty?
++ if fmt = params[:format] && _provided_formats.include?(fmt.to_sym)
++ return fmt.to_sym
+ end
++ accepts = Responder.parse(request.accept).map {|t| t.to_sym}
++ return _provided_formats.first if accepts.include?(:all)
++ (accepts & _provided_formats).first || (raise Merb::ControllerExceptions::NotAcceptable)
++ end
+
+- # Returns the output format for this request, based on the
+- # provided formats, <tt>params[:format]</tt> and the client's HTTP
+- # Accept header.
+- #
+- # The first time this is called, it triggers content negotiation
+- # and caches the value. Once you call +content_type+ you can
+- # not set or change the list of provided formats.
+- #
+- # Called automatically by +render+, so you should only call it if
+- # you need the value, not to trigger content negotiation.
+- #
+- # ==== Parameters
+- # fmt<String?>::
+- # An optional format to use instead of performing content negotiation.
+- # This can be used to pass in the values of opts[:format] from the
+- # render function to short-circuit content-negotiation when it's not
+- # necessary. This optional parameter should not be considered part
+- # of the public API.
+- #
+- # ==== Returns
+- # Symbol:: The content-type that will be used for this controller.
+- #
+- #---
+- # @public
+- def content_type(fmt = nil)
+- self.content_type = (fmt || _perform_content_negotiation) unless @_content_type
+- @_content_type
++ # Returns the output format for this request, based on the
++ # provided formats, <tt>params[:format]</tt> and the client's HTTP
++ # Accept header.
++ #
++ # The first time this is called, it triggers content negotiation
++ # and caches the value. Once you call +content_type+ you can
++ # not set or change the list of provided formats.
++ #
++ # Called automatically by +render+, so you should only call it if
++ # you need the value, not to trigger content negotiation.
++ #
++ # ==== Parameters
++ # fmt<String?>::
++ # An optional format to use instead of performing content negotiation.
++ # This can be used to pass in the values of opts[:format] from the
++ # render function to short-circuit content-negotiation when it's not
++ # necessary. This optional parameter should not be considered part
++ # of the public API.
++ #
++ # ==== Returns
++ # Symbol:: The content-type that will be used for this controller.
++ #
++ #---
++ # @public
++ def content_type(fmt = nil)
++ @_content_type = (fmt || _perform_content_negotiation) unless @_content_type
++ @_content_type
++ end
++
++ # Sets the content type of the current response to a value based on
++ # a passed in key. The Content-Type header will be set to the first
++ # registered header for the mime-type.
++ #
++ # ==== Parameters
++ # type<Symbol>:: A type that is in the list of registered mime-types.
++ #
++ # ==== Raises
++ # ArgumentError:: "type" is not in the list of registered mime-types.
++ #
++ # ==== Returns
++ # Symbol:: The content-type that was passed in.
++ #
++ #---
++ # @semipublic
++ def content_type=(type)
++ unless Merb.available_mime_types.has_key?(type)
++ raise Merb::ControllerExceptions::NotAcceptable.new("Unknown content_type for response: #{type}")
++ end
++ headers['Content-Type'] = Merb.available_mime_types[type].first
++ @_content_type = type
++ end
++
++ end
++
++ class Responder
++
++ protected
++ def self.parse(accept_header)
++ # parse the raw accept header into a unique, sorted array of AcceptType objects
++ list = accept_header.to_s.split(/,/).enum_for(:each_with_index).map do |entry,index|
++ AcceptType.new(entry,index += 1)
++ end.sort.uniq
++ # firefox (and possibly other browsers) send broken default accept headers.
++ # fix them up by sorting alternate xml forms (namely application/xhtml+xml)
++ # ahead of pure xml types (application/xml,text/xml).
++ if app_xml = list.detect{|e| e.super_range == 'application/xml'}
++ list.select{|e| e.to_s =~ /\+xml/}.each { |acc_type|
++ list[list.index(acc_type)],list[list.index(app_xml)] =
++ list[list.index(app_xml)],list[list.index(acc_type)] }
+ end
+-
+- # Sets the content type of the current response to a value based on
+- # a passed in key. The Content-Type header will be set to the first
+- # registered header for the mime-type.
+- #
+- # ==== Parameters
+- # type<Symbol>:: A type that is in the list of registered mime-types.
+- #
+- # ==== Raises
+- # ArgumentError:: "type" is not in the list of registered mime-types.
+- #
+- # ==== Returns
+- # Symbol:: The content-type that was passed in.
+- #
+- #---
+- # @semipublic
+- def content_type=(type)
+- unless Merb.available_mime_types.has_key?(type)
+- raise Merb::ControllerExceptions::NotAcceptable.new("Unknown content_type for response: #{type}")
+- end
+- headers['Content-Type'] = Merb.available_mime_types[type].first
+- @_content_type = type
++ list
++ end
++
++ public
++ def self.params_to_query_string(value, prefix = nil)
++ case value
++ when Array
++ value.map { |v|
++ params_to_query_string(v, "#{prefix}[]")
++ } * "&"
++ when Hash
++ value.map { |k, v|
++ params_to_query_string(v, prefix ? "#{prefix}[#{Merb::Request.escape(k)}]" : Merb::Request.escape(k))
++ } * "&"
++ else
++ "#{prefix}=#{Merb::Request.escape(value)}"
+ end
++ end
+
+- end
++ end
++
++ class AcceptType
++
++ attr_reader :media_range, :quality, :index, :type, :sub_type
+
++ def initialize(entry,index)
++ @index = index
++ @media_range, quality = entry.split(/;\s*q=/).map{|a| a.strip }
++ @type, @sub_type = @media_range.split(/\//)
++ quality ||= 0.0 if @media_range == '*/*'
++ @quality = ((quality || 1.0).to_f * 100).to_i
++ end
++
++ def <=>(entry)
++ c = entry.quality <=> quality
++ c = index <=> entry.index if c == 0
++ c
++ end
++
++ def eql?(entry)
++ synonyms.include?(entry.media_range)
++ end
++
++ def ==(entry); eql?(entry); end
++
++ def hash; super_range.hash; end
++
++ def synonyms
++ @syns ||= Merb.available_mime_types.values.map do |e|
++ e[:request_headers] if e[:request_headers].include?(@media_range)
++ end.compact.flatten
++ end
++
++ def super_range
++ synonyms.first || @media_range
++ end
++
++ def to_sym
++ Merb.available_mime_types.select{|k,v|
++ v[:request_headers] == synonyms || v[:request_headers][0] == synonyms[0]}.flatten.first
++ end
++
++ def to_s
++ @media_range
++ end
++
+ end
++
+
+ end
+\ No newline at end of file
+diff --git a/lib/merb_core/dispatch/dispatcher.rb b/lib/merb_core/dispatch/dispatcher.rb
+index c458c9f9ad454d3b0c3055d6b2a8e88b17712b44..f7fed0f539a20f9cce08b72c551725ad0563bf37 100644
+--- a/lib/merb_core/dispatch/dispatcher.rb
++++ b/lib/merb_core/dispatch/dispatcher.rb
+@@ -33,10 +33,10 @@ class Merb::Dispatcher
+
+ # this is the custom dispatch_exception; it allows failures to still be dispatched
+ # to the error controller
+- rescue => exception
+- Merb.logger.error(Merb.exception(exception))
+- exception = controller_exception(exception)
+- dispatch_exception(request, response, exception)
++ # rescue => exception
++ # Merb.logger.error(Merb.exception(exception))
++ # exception = controller_exception(exception)
++ # dispatch_exception(request, response, exception)
+ end
+
+ private
+@@ -49,10 +49,10 @@ class Merb::Dispatcher
+ def dispatch_action(klass, action, request, response, status=200)
+ # build controller
+ controller = klass.build(request, response, status)
+- if @@use_mutex
+- @@mutex.synchronize { controller.dispatch(action) }
++ if use_mutex
++ @@mutex.synchronize { controller._dispatch(action) }
+ else
+- controller.dispatch(action)
++ controller._dispatch(action)
+ end
+ [controller, action]
+ end
+diff --git a/lib/merb_core/rack/adapter.rb b/lib/merb_core/rack/adapter.rb
+index ffc7117e9733e83b0567bbe4a43fac7663800b7d..217399a5382d0b3878aaea3d3e302173c5b5f119 100644
+--- a/lib/merb_core/rack/adapter.rb
++++ b/lib/merb_core/rack/adapter.rb
+@@ -40,7 +40,7 @@ module Merb
+ begin
+ controller, action = ::Merb::Dispatcher.handle(request, response)
+ rescue Object => e
+- return [500, {"Content-Type"=>"text/html"}, "Internal Server Error"]
++ return [500, {"Content-Type"=>"text/html"}, e.message + "<br/>" + e.backtrace.join("<br/>")]
+ end
+ [controller.status, controller.headers, controller.body]
+ end
+diff --git a/lib/merb_core/test/request_helper.rb b/lib/merb_core/test/request_helper.rb
+index 10a9fb3ace56eaf1db0fa300df3fb2ab88a7118a..f302a3b71539182ba142cd208fe6d6aae171b1a1 100644
+--- a/lib/merb_core/test/request_helper.rb
++++ b/lib/merb_core/test/request_helper.rb
+@@ -26,8 +26,10 @@ module Merb::Test::RequestHelper
+ Merb::Test::FakeRequest.new(env, StringIO.new(req))
+ end
+
+- def dispatch_to(controller_klass, action, env = {}, opt = {}, &blk)
+- request = fake_request(env, opt)
++ def dispatch_to(controller_klass, action, params = {}, env = {}, &blk)
++ request = fake_request(env,
++ :query_string => Merb::Responder.params_to_query_string(params))
++
+ controller = controller_klass.build(request)
+ controller.instance_eval(&blk) if block_given?
+ controller._dispatch(action)
+diff --git a/spec/public/abstract_controller/spec_helper.rb b/spec/public/abstract_controller/spec_helper.rb
+index df759008d14e7572b5c44de24f77f828f83f1682..694cee2592a210a5c1fa40ca7846beeaa09725fe 100644
+--- a/spec/public/abstract_controller/spec_helper.rb
++++ b/spec/public/abstract_controller/spec_helper.rb
+@@ -1,12 +1,10 @@
+ __DIR__ = File.dirname(__FILE__)
+ require File.join(__DIR__, "..", "..", "spec_helper")
+
+-# The framework structure *must* be set up before loading in framework
+-# files.
+ require File.join(__DIR__, "controllers", "filters")
+ require File.join(__DIR__, "controllers", "render")
+
+-Merb::BootLoader::Templates.new.run
++Merb::BootLoader::Templates.run
+
+ module Merb::Test::Behaviors
+ def dispatch_should_make_body(klass, body, action = :index)
+diff --git a/spec/public/controller/base_spec.rb b/spec/public/controller/base_spec.rb
+index 1709e612629ed2c2b6af4579a8b89684aca9aa3c..5bcdb59948cc22592639b1aee9bd233ff2c306fa 100644
+--- a/spec/public/controller/base_spec.rb
++++ b/spec/public/controller/base_spec.rb
+@@ -10,11 +10,11 @@ describe Merb::Controller, " callable actions" do
+ end
+
+ it "should dispatch to callable actions" do
+- dispatch_to(Merb::Test::Fixtures::TestFoo, :index).body.should == "index"
++ dispatch_to(Merb::Test::Fixtures::TestBase, :index).body.should == "index"
+ end
+
+ it "should not dispatch to hidden actions" do
+- calling { dispatch_to(Merb::Test::Fixtures::TestFoo, :hidden) }.
++ calling { dispatch_to(Merb::Test::Fixtures::TestBase, :hidden) }.
+ should raise_error(Merb::ControllerExceptions::ActionNotFound)
+ end
+
+diff --git a/spec/public/controller/controllers/base.rb b/spec/public/controller/controllers/base.rb
+index a1b3beb27899df781d943427d9b23945f02e14de..c4b69a440a9da3c3486208d2cb95ccb8bdb974b9 100644
+--- a/spec/public/controller/controllers/base.rb
++++ b/spec/public/controller/controllers/base.rb
+@@ -3,7 +3,7 @@ module Merb::Test::Fixtures
+ self._template_root = File.dirname(__FILE__) / "views"
+ end
+
+- class TestFoo < ControllerTesting
++ class TestBase < ControllerTesting
+ def index
+ "index"
+ end
+diff --git a/spec/public/controller/controllers/responder.rb b/spec/public/controller/controllers/responder.rb
+new file mode 100644
+index 0000000000000000000000000000000000000000..867192e8f6e995a43fd5cd3daffa0ec11b3d31e5
+--- /dev/null
++++ b/spec/public/controller/controllers/responder.rb
+@@ -0,0 +1,25 @@
++module Merb::Test::Fixtures
++ class ControllerTesting < Merb::Controller
++ self._template_root = File.dirname(__FILE__) / "views"
++ end
++
++ class TestResponder < ControllerTesting
++ def index
++ render
++ end
++ end
++
++ class TestHtmlDefault < TestResponder; end
++
++ class TestClassProvides < TestResponder;
++ provides :xml
++ end
++
++ class TestLocalProvides < TestResponder;
++ def index
++ provides :xml
++ render
++ end
++ end
++
++end
+\ No newline at end of file
+diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.html.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.html.erb
+new file mode 100644
+index 0000000000000000000000000000000000000000..1bfb77d4a44c444bba6888ae7740f7df4b074c58
+--- /dev/null
++++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.html.erb
+@@ -0,0 +1 @@
++This should not be rendered
+\ No newline at end of file
+diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.xml.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.xml.erb
+new file mode 100644
+index 0000000000000000000000000000000000000000..7c91f633987348e87e5e34e1d9e87d9dd0e5100c
+--- /dev/null
++++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.xml.erb
+@@ -0,0 +1 @@
++<XML:Class provides='true' />
+\ No newline at end of file
+diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_html_default/index.html.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_html_default/index.html.erb
+new file mode 100644
+index 0000000000000000000000000000000000000000..eb4b52bf5a7aaba8f1706de419f42789c05684a2
+--- /dev/null
++++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_html_default/index.html.erb
+@@ -0,0 +1 @@
++HTML: Default
+\ No newline at end of file
+diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.html.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.html.erb
+new file mode 100644
+index 0000000000000000000000000000000000000000..a3a841a89c62e6174038935a42da9cd24ff54413
+--- /dev/null
++++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.html.erb
+@@ -0,0 +1 @@
++This should not render
+\ No newline at end of file
+diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.xml.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.xml.erb
+new file mode 100644
+index 0000000000000000000000000000000000000000..c1384ec6af0357b585cc367035d1bc3a30347ade
+--- /dev/null
++++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.xml.erb
+@@ -0,0 +1 @@
++<XML:Local provides='true' />
+\ No newline at end of file
+diff --git a/spec/public/controller/responder_spec.rb b/spec/public/controller/responder_spec.rb
+index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bcf18532442e5965cf6ca8501770d7b7a1eb2429 100644
+--- a/spec/public/controller/responder_spec.rb
++++ b/spec/public/controller/responder_spec.rb
+@@ -0,0 +1,31 @@
++require File.join(File.dirname(__FILE__), "spec_helper")
++
++describe Merb::Controller, " responds" do
++
++ before do
++ Merb.push_path(:layout, File.dirname(__FILE__) / "controllers" / "views" / "layouts")
++ Merb::Router.prepare do |r|
++ r.default_routes
++ end
++ end
++
++ it "should default the mime-type to HTML" do
++ dispatch_to(Merb::Test::Fixtures::TestHtmlDefault, :index).body.should == "HTML: Default"
++ end
++
++ it "should use other mime-types if they are provided on the class level" do
++ controller = dispatch_to(Merb::Test::Fixtures::TestClassProvides, :index, {}, :http_accept => "application/xml")
++ controller.body.should == "<XML:Class provides='true' />"
++ end
++
++ it "should fail if none of the acceptable mime-types are available" do
++ calling { dispatch_to(Merb::Test::Fixtures::TestClassProvides, :index, {}, :http_accept => "application/json") }.
++ should raise_error(Merb::ControllerExceptions::NotAcceptable)
++ end
++
++ it "should use mime-types that are provided at the local level" do
++ controller = dispatch_to(Merb::Test::Fixtures::TestLocalProvides, :index, {}, :http_accept => "application/xml")
++ controller.body.should == "<XML:Local provides='true' />"
++ end
++
++end
+\ No newline at end of file
+diff --git a/spec/public/controller/spec_helper.rb b/spec/public/controller/spec_helper.rb
+index f68628a63740f4ce0235a15d71c5889e55ecaf78..e360194c1fbaf72c3298c61543c2d3a19b512b41 100644
+--- a/spec/public/controller/spec_helper.rb
++++ b/spec/public/controller/spec_helper.rb
+@@ -1,4 +1,10 @@
+ __DIR__ = File.dirname(__FILE__)
++require 'ruby-debug'
++
+ require File.join(__DIR__, "..", "..", "spec_helper")
+
+-require File.join(__DIR__, "controllers", "base")
+\ No newline at end of file
++require File.join(__DIR__, "controllers", "base")
++require File.join(__DIR__, "controllers", "responder")
++
++Merb::BootLoader::Templates.run
++Merb::BootLoader::MimeTypes.run
+\ No newline at end of file
diff --git a/test/fixtures/diff_new_mode b/test/fixtures/diff_new_mode
new file mode 100644
index 00000000..663c9099
--- /dev/null
+++ b/test/fixtures/diff_new_mode
@@ -0,0 +1,14 @@
+diff --git a/conf/global_settings.py b/conf/global_settings.py
+old mode 100644
+new mode 100755
+index 9ec1bac..1c4f83b
+--- a/conf/global_settings.py
++++ b/conf/global_settings.py
+@@ -58,6 +58,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
+ )
+
+ MIDDLEWARE_CLASSES = (
++ "django.middleware.cache.CacheMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
diff --git a/test/fixtures/diff_numstat b/test/fixtures/diff_numstat
new file mode 100644
index 00000000..44c6ca2d
--- /dev/null
+++ b/test/fixtures/diff_numstat
@@ -0,0 +1,2 @@
+29 18 a.txt
+0 5 b.txt
diff --git a/test/fixtures/diff_p b/test/fixtures/diff_p
new file mode 100644
index 00000000..af4759e5
--- /dev/null
+++ b/test/fixtures/diff_p
@@ -0,0 +1,610 @@
+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
++ # => [#<GitPython.Commit "e80bbd2ce67651aa18e57fb0b43618ad4baf7750">,
++ #<GitPython.Commit "91169e1f5fa4de2eaea3f176461f5dc784796769">,
++ #<GitPython.Commit "038af8c329ef7c1bae4568b98bd5c58510465493">,
++ #<GitPython.Commit "40d3057d09a7a4d61059bca9dca5ae698de58cbe">,
++ #<GitPython.Commit "4ea50f4754937bf19461af58ce3b3d24c77311d9">]
++
++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
++ # => [#<GitPython.Commit "91169e1f5fa4de2eaea3f176461f5dc784796769">]
++
++ head.tree
++ # => #<GitPython.Tree "3536eb9abac69c3e4db583ad38f3d30f8db4771f">
++
++ head.author
++ # => #<GitPython.Actor "Tom Preston-Werner <tom@mojombo.com>">
++
++ head.authored_date
++ # => Wed Oct 24 22:02:31 -0700 2007
++
++ head.committer
++ # => #<GitPython.Actor "Tom Preston-Werner <tom@mojombo.com>">
++
++ 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
++ # => #<GitPython.Tree "3536eb9abac69c3e4db583ad38f3d30f8db4771f">
++
++ tree.id
++ # => "3536eb9abac69c3e4db583ad38f3d30f8db4771f"
++
++Once you have a tree, you can get the contents.
++
++ contents = tree.contents
++ # => [#<GitPython.Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">,
++ #<GitPython.Blob "81d2c27608b352814cbe979a6acd678d30219678">,
++ #<GitPython.Tree "c3d07b0083f01a6e1ac969a0f32b8d06f20c62e5">,
++ #<GitPython.Tree "4d00fe177a8407dbbc64a24dbfc564762c0922d8">]
++
++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"
++ # => #<GitPython.Tree "e74893a3d8a25cbb1367cf241cc741bfd503c4b2">
++
++You can also get a tree directly from the repo if you know its name.
++
++ repo.tree
++ # => #<GitPython.Tree "master">
++
++ repo.tree("91169e1f5fa4de2eaea3f176461f5dc784796769")
++ # => #<GitPython.Tree "91169e1f5fa4de2eaea3f176461f5dc784796769">
++
++= The Blob object
++
++A blob represents a file. Trees often contain blobs.
++
++ blob = tree.contents.first
++ # => #<GitPython.Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">
++
++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")
++ # => #<GitPython.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 <jdoe@example.com>
++ #
++ # 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{#<GitPython.Actor "#{@name} <#{@email}>">}
++ 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{#<GitPython.Blob "#{@id}">}
+ 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 <tom@example.com>")
++ 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 <tom@example.com>")
++ assert_equal %Q{#<GitPython.Actor "Tom Werner <tom@example.com>">}, 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 <tom@mojombo.com>", @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 <tom@mojombo.com>", 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 <tom@mojombo.com>", 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{#<GitPython.Repo "#{File.expand_path(GRIT_REPO)}/.git">}, @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
diff --git a/test/fixtures/for_each_ref b/test/fixtures/for_each_ref
new file mode 100644
index 00000000..e56f5262
--- /dev/null
+++ b/test/fixtures/for_each_ref
Binary files differ
diff --git a/test/fixtures/for_each_ref_tags b/test/fixtures/for_each_ref_tags
new file mode 100644
index 00000000..c4df85c6
--- /dev/null
+++ b/test/fixtures/for_each_ref_tags
Binary files differ
diff --git a/test/fixtures/ls_tree_a b/test/fixtures/ls_tree_a
new file mode 100644
index 00000000..69b76f4a
--- /dev/null
+++ b/test/fixtures/ls_tree_a
@@ -0,0 +1,7 @@
+100644 blob 81d2c27608b352814cbe979a6acd678d30219678 History.txt
+100644 blob 641972d82c6d1b51122274ae8f6a0ecdfb56ee22 Manifest.txt
+100644 blob 8b1e02c0fb554eed2ce2ef737a68bb369d7527df README.txt
+100644 blob 735d7338b7cb208563aa282f0376c5c4049453a7 Rakefile
+040000 tree c3d07b0083f01a6e1ac969a0f32b8d06f20c62e5 bin
+040000 tree aa06ba24b4e3f463b3c4a85469d0fb9e5b421cf8 lib
+040000 tree 650fa3f0c17f1edb4ae53d8dcca4ac59d86e6c44 test
diff --git a/test/fixtures/ls_tree_b b/test/fixtures/ls_tree_b
new file mode 100644
index 00000000..329aff39
--- /dev/null
+++ b/test/fixtures/ls_tree_b
@@ -0,0 +1,2 @@
+100644 blob aa94e396335d2957ca92606f909e53e7beaf3fbb grit.rb
+040000 tree 34868e6e7384cb5ee51c543a8187fdff2675b5a7 grit
diff --git a/test/fixtures/ls_tree_commit b/test/fixtures/ls_tree_commit
new file mode 100644
index 00000000..d97aca04
--- /dev/null
+++ b/test/fixtures/ls_tree_commit
@@ -0,0 +1,3 @@
+040000 tree 2afb47bcedf21663580d5e6d2f406f08f3f65f19 foo
+160000 commit d35b34c6e931b9da8f6941007a92c9c9a9b0141a bar
+040000 tree f623ee576a09ca491c4a27e48c0dfe04be5f4a2e baz
diff --git a/test/fixtures/rev_list b/test/fixtures/rev_list
new file mode 100644
index 00000000..95a1ebff
--- /dev/null
+++ b/test/fixtures/rev_list
@@ -0,0 +1,24 @@
+commit 4c8124ffcf4039d292442eeccabdeca5af5c5017
+tree 672eca9b7f9e09c22dcb128c283e8c3c8d7697a4
+parent 634396b2f541a9f2d58b00be1a07f0c358b999b3
+author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
+committer Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
+
+ implement Grit#heads
+
+commit 634396b2f541a9f2d58b00be1a07f0c358b999b3
+tree b35b4bf642d667fdd613eebcfe4e17efd420fb8a
+author Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
+committer Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
+
+ initial grit setup
+
+commit ab25fd8483882c3bda8a458ad2965d2248654335
+tree c20b5ec543bde1e43a931449b196052c06ed8acc
+parent 6e64c55896aabb9a7d8e9f8f296f426d21a78c2c
+parent 7f874954efb9ba35210445be456c74e037ba6af2
+author Tom Preston-Werner <tom@mojombo.com> 1182645538 -0700
+committer Tom Preston-Werner <tom@mojombo.com> 1182645538 -0700
+
+ Merge branch 'site'
+ Some other stuff
diff --git a/test/fixtures/rev_list_commit_diffs b/test/fixtures/rev_list_commit_diffs
new file mode 100644
index 00000000..20397e2e
--- /dev/null
+++ b/test/fixtures/rev_list_commit_diffs
@@ -0,0 +1,8 @@
+commit 91169e1f5fa4de2eaea3f176461f5dc784796769
+tree 802ed53edbf6f02ad664af3f7e5900f514024b2f
+parent 038af8c329ef7c1bae4568b98bd5c58510465493
+author Tom Preston-Werner <tom@mojombo.com> 1193200199 -0700
+committer Tom Preston-Werner <tom@mojombo.com> 1193200199 -0700
+
+ fix some initialization warnings
+
diff --git a/test/fixtures/rev_list_commit_idabbrev b/test/fixtures/rev_list_commit_idabbrev
new file mode 100644
index 00000000..9385ba71
--- /dev/null
+++ b/test/fixtures/rev_list_commit_idabbrev
@@ -0,0 +1,8 @@
+commit 80f136f500dfdb8c3e8abf4ae716f875f0a1b57f
+tree 3fffd0fce0655433c945e6bdc5e9f338b087b211
+parent 44f82e5ac93ba322161019dce44b78c5bd1fdce2
+author tom <tom@taco.(none)> 1195608462 -0800
+committer tom <tom@taco.(none)> 1195608462 -0800
+
+ fix tests on other machines
+
diff --git a/test/fixtures/rev_list_commit_stats b/test/fixtures/rev_list_commit_stats
new file mode 100644
index 00000000..60aa8cf5
--- /dev/null
+++ b/test/fixtures/rev_list_commit_stats
@@ -0,0 +1,7 @@
+commit 634396b2f541a9f2d58b00be1a07f0c358b999b3
+tree b35b4bf642d667fdd613eebcfe4e17efd420fb8a
+author Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
+committer Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
+
+ initial grit setup
+
diff --git a/test/fixtures/rev_list_count b/test/fixtures/rev_list_count
new file mode 100644
index 00000000..a802c139
--- /dev/null
+++ b/test/fixtures/rev_list_count
@@ -0,0 +1,655 @@
+72223ed47d7792924083f1966e550694a0259d36
+f7cd338ee316482c478805aa8b636a33df3e4299
+994566139b90fffdc449c3f1104f42626e90f89f
+e34590b7a2d186b3bb9a1170d02d52b36c791c78
+8977833d74f8681aa0d9a5e84b0dd3d81519774d
+6f5561530cb3a94e4c86454e84732197325be172
+ee419e04a961543444be6db66aef52e6e37936d6
+d845de9d438e1a249a0c2fcb778e8ea3b7e06cef
+0bba4a6c10060405a94d52533af2f9bdacd4f29c
+77711c0722964ead965e0ba2ee9ed4a03cb3d292
+501d23cac6dd911511f15d091ee031a15b90ebde
+07c9bd0abcd47cf9ca68af5d2403e28de33154f1
+103ca320fc8bd48fed16e074df6ace6562bed4b5
+55544624fb9be54a3b9f9e2ec85ef59e08bd0376
+e5c8246dec64eccad0c095c67f5a8bbea7f11aca
+1b54d9f82ee6f3f2129294c85fad910178bef185
+36062a1634fb25de2c4b8f6b406ae3643805baf5
+0896bb9b8d2217163e78b5f1f75022a330d9ddc8
+6646dfce607b043ab7bbe36e51321422673b7c56
+f0bad592abc255fabe6c6d6c62b604b3de5cdce2
+5705e15c71f9e10ca617c0a234b37609cfb74d23
+b006d8b73912eb028355c49e7bfe53a29f97ce7c
+b21eb6173dbe07cac63f4571e353188dde46f049
+a3256f3150ccec73c50b61b85d54e30e39a65848
+c5a32e937166d65757f3dd4c1b6fd4f5ecc10970
+1e90e2c654aab0d81088f615c090d6d46f03ca4c
+924e7685fcd62d83aac19480837e4edd9c4bae5e
+489e1468aea658a333481132957069708127c69f
+970b6b13726889f6883e4347e34d8f9d66deb7c9
+df74c45e8fdb4d29a7de265ac08d0bff76b78a83
+936aa0b946155b2d47528073433fc08b17a4c7cc
+3b6a5e8f12b6269a0a3e0eaeede81abfb3fc4896
+8e0f306dae96d78aa1ea9a08e82647fd95fc1a74
+5eb099e5e274be44c0fd27ce8928d2dc8483dab7
+050fbed693d4806ac6c03460103777b2a4befcf8
+c5d4b6dac74e323d474fa8878a7ea0c233d57019
+8e5daf911943d5ef025025c137fcf97164467141
+bcdf7c2421302b15f4ee4ebbdeae7b644a4518e7
+e2874a42835cbb2fe8856a398f5c4b49a9cd8d30
+f50ea97159e4ae7132e057fbf5ea1e75ec961282
+5dbd614c20e9473240082239894d99c24de42282
+0490e1ac1ffafcb9117029286b224ab39671a015
+ad3620d47f0ea96f24904227d3c7a7f9548c34dd
+fd37e7191ae3d312ced0877a1492cd2ea4578275
+b7f8cc51c9056a469006b5601a4993b67c07e099
+1d849af5083073b8864487938a9a2a8e21d71529
+26d0bb4c9ee3d8591fe291c86f495b2d1900bf9b
+7a25e3056a7225c1ff8542c2c2c1cf6f3a8e13d4
+d0e0de0b13b9c81d2bcf9d54eecdb20591fd6d2f
+0bf82343ade1e07c0aebd14ee66df688a4cc0e87
+d81de0fb6a19342a90cdba9a664659da66296162
+9105667175797bbadea80879e98a5cf849a40065
+12f5af2a169c658cfae1677ceafd527d3775873f
+00ae94689600b5949bd0fcf68205f31f95a36aa4
+8f5d34224e4620c51c16c01578786e76567d025d
+3385eb31651c84384b4c7e93d82bc5b592edf9fb
+eda9179b9af0275d62c4074480e7a0103d356435
+982c2d1e55165fddb4f4c69065e2c4ac39542c84
+7117495ef012719769582939ea59a5533077fc8f
+b7dae75dab5b59a320b8df8a67060d238fed3a8e
+37c684e1a46599fe4d34d1601875685a70b1b879
+0a694fa0cb044a31bb844564776b490c151ac317
+e77c6b459f01ce078aa59183189226a6d48fdf38
+dd0c0eaefdebc38610bb1986e51324a0392e829a
+d8bc2414e9504172da640f29db1b2d29d834a94b
+a9f1119667dd0f5aa9413dec23965a747d1dac05
+f52775f6bc21d999382f4b9b38b108b814114ea1
+e82c77ac679887140867e471a9f47fd3b2d95039
+2db3fff5673bbd4bfcc8139d8398727d243c9efd
+c1805c000c6233a20ac0824cad21c1fe40f93100
+83f7807585cf70018a9a06179af9d89d4a8204b2
+730c326beb29cc6d2624915b125633792a40ca36
+bea422b653d74dd03ec068dcce938169149aa822
+586a57666618299464519c450033eecc3ce89664
+82fba8cf4796f2adbec5ad336bd750ad60a075fd
+9d9b899f836a199fe854075e99091d1ef083de24
+4670357c662596aa2c2922d826de84abd9f877ea
+9b562567430544c74009ea4a6173f44ddb4a44e5
+013d51fbb5f3a60bc748449b1ab73158da9a3203
+3fe67cb90fca9ea76292deb793cb480f4eb5e8d6
+91c80e489fee08e71a79bfbea79fcc28e1aa27f2
+dd9104095bdb08fe399af46d91b334e760986ddd
+a9198904586546a038f855bc6fc0e7cc413722fb
+574a7bad1017d9ed466474881e1f068f892207f4
+f95acec9297b7816284d8b24e984cd5c82104c89
+3907dac65a125b7759172a8eae959b0e70220299
+e5b44576eb2182b16c7b6770fab5977eedbc03c6
+9f4aad9833d0f9a609dd2556e7db784ba813d8fa
+579309c96651a1fed75fdd18f80019db8e6624ec
+5e1a9a48e6c96099d6a0c3aff1e31c9be16b7b8d
+cae4b811038f4e0dd4a8e68122c3db955e10ae81
+fccee1c818f5af5fce593de0949f5a8ecd35b443
+d4187d5a5f9ffe1f882c74f6ced7e0ba1c260ff2
+02ff197aa41d892e623dc668b0055806294bd6c0
+3f81af24214761a6ed77fd4dcd6e45a651dd8f84
+5cb08c5232a669a881606a6d8c4a4cd23aad6755
+5212b25869e0b9aff485af6f5371a159e89f8f07
+a778322bb60f8438a68112a73df78e05a97093ff
+b55c30c3992a766628dfc4a7e22db4d8d9e46b5f
+1d3e4a32e0407f16f641be316c745c1a48f16e2b
+7f35ca3333944165e0ec82a3a95c185f67fba904
+ef6c5bbe2dffe76e4a9698df03b8ee08af702033
+aeb90405ed696c1efcb29d0664b43a52a2bf824f
+e0b8bebd78172229666dfd425933f9bc21863928
+2a71a55154edf75ab51dcb4f2f7dc63592410e16
+a5d25352d326c77d083a2e089c2d80b4ea923624
+a3fbc38b9f1b86bb5f5e6540048083fc1dc6062b
+bbe67e1bdadf4aff186769145a40727f78e39e01
+a02a58c6c6d04001873ba91ac3dc902275879d0f
+eb5281d4f40e18b0e21d275ee5c5964bbbcc855c
+19e9939a098b9cb93c8c1d0d069d46861afb507a
+7a72471f9a4587cc4a7d37da0d26122b0eadaddc
+c6a043eb057cd544130b77bf99f39b7738e0a204
+723b6223726c6772e034d9f4ba5c710e66a1991f
+25b4ff1a26dd3694a98c1ef2eba04a5a500c0b28
+7c571ac2c35a7e1f399651242e904596c93beeb0
+0c90015733521720688bfcb59ad2a3978b2fbbc3
+d6b99183122a97a590e4e54f4617b58f13b90df8
+6b663271af39d69082422866e61ff7801c2b3fa7
+2e9e6ab7651e4c215110eb381678e0ea2bc0f7d8
+967b91e045661c9b6d2a5f011ec152da391db7ec
+7fda8d15bdb3d3d61fce49413153a216849721f9
+f7d7e83ee1cec103a768ddc9f68b6d5075849894
+925953da542a9c21a3fde1ab0891175fb6212a12
+ea2f54455427506583437391cbaf470a1ef4edeb
+f0bdb2cdddefb3a391ec2e3fa9b78ed06d7c874a
+8d289566fa92a96a83ff3c2e24c1f3d12b1718ed
+7fb102615532952c6833e87389668831b37a13d6
+7f7bbe8473158ab606a89ad66d607ffd0e5ba1f7
+a98ea5a00d19406f3e644448039f13db496cefd5
+39f03072d9d84d622ae974b09dd11cf7a2515a7c
+e2050a1c488fff4b114614d7f75096dd0a149f5b
+d2851f113530fbe211b3e948b6181152d30d1fa3
+1eef0fe740f6db35a91e790fe77d4ba1c9065e99
+9608403b012908cc58223db44962553704cab8af
+4911a005ea6b55f34f8b0f504a6a0934c0df896a
+a4400fb8e7d0f1261634dbb89588da86b8b6c93f
+f310729583f6733ee60f534a9732b7a3a9e414d4
+49e78793487ce4d8d7e624b5245fca8a9cc1ba66
+2f2501ce5d28e5ada6018504ee8dcecbbee70428
+f1e127253e1eb07b537b221e9cc96beb16333790
+8bf1684ca9b5a37d91671dd0d63d0ac59bea987a
+24838a6042a134b11fe945bbaa5ab1b2b3fc6eb0
+f53c57af21fded3735fd479b3785fcf7adf80268
+aa8d0a63d61d13524b1395972563b516cd263f05
+16803d64332412a66121ef3fd10cd0d88598d3be
+5f2715ed4d9416fa4940c2cd29b5ca18b6a79b8c
+851ede1f8dceef7d681f35e4769e5693160c0a04
+5264588c6c20c38d54394059eef0a854683aa3fc
+111800d8e66ff86f0757df7eb6533fc62040a22f
+b04de89d31003e468c191cd08dd2a4629d99c38e
+6aef629094e9ee6b4fac2431897844c4dddf2f57
+d1168c999fdae7d1eaac8c163b2b1190afb1815c
+6afc3257929528d9f4de964e8828822d2fa2c93a
+436f30ce1b562efe4f34696def45b0145eb98304
+9afbf904be0e6154f6c424377ad596e86ea38807
+a3cf657305d9283525711e867e03684a2e4b39cc
+5813b4d04b25c385359af4437027b4fe763cd2ba
+0fa594594c97a0d3579312f4ec073304c1436106
+cb7b36c28adb38b1e597fa3f3b5c24c988a25b0e
+5b0c867cbda81ce34df1b5fb67557b556ea24e9e
+44090e9c550c7c5ded01dc2a681a7c934ba901b6
+9ccc89b61736c4a9c02faaa679e97a9ec063dd29
+7828d6d18115b0720888a45e3b547b697910c59e
+618497e48e46fdc00dee67c07cd0f40860e819f9
+69a14ed4f36d880e8322a530d8c5bfd9888a8c13
+0a0cd655e40903abff4840c23b57628fb1a88122
+cb262098646f47e1d80a89662f1480c216bfd81b
+d60e59fce6f698a8bb97e2b4a935c069584621b1
+ca77ba0d6d301cee1d45edb24742dc5cdabd4b83
+17b598510967922690f5181903f20ddae5758e86
+30ad3d9f3164966afb2974640f772387fb796b7d
+48964c5dcc94234dea1737d7fa23220f9eab0fb7
+0fe241f4db12f455c2f5976c6bf6497cc485f503
+04953aca41bd372d990da7f68cc795f4a8b78d94
+2dc9a061595a291d8c53168c42da8d14da52d610
+68b15d34903038e3f2e327f03f0486b2d38784bc
+30ceaaf39b10f9f9c7b4362505144d1650035a40
+e75891a5760f6a51f54a33b671951c16fbce1558
+b2a35989ad3392f26e070b158f89d1d8b75327f2
+8468830b8b37f7c1cdda926a966c0aba2893a7c0
+6a6112e8cde1bafebfa12e4c863dab5834c38e12
+eafcd2ffc25d17fce41eff2afd5c4521730a23ab
+f7eda0752f45c3a4eb13e075b24b86d7e7dd5016
+b634d0d48d0a113bc060a47972b10c9353621428
+49f95235a174f0a056e44bb5c704fea4ab345353
+6eec70a31a6376ffd7d6b143be0968a195ad59d6
+7c9ae1a71aa39efe28a678c18c8a03d759deabed
+a19fd6f13c16720dc38a1f572eebf960022015ad
+87052ac2cbaec195094e1d1a2bad4ac480bd111e
+2cde1b0e69f97a8a67bb47d729c53af3ba8e5700
+91a06d1a4efb6376959c3b444a536fe6b4fd4d6b
+07f73b465b6c509b337c2776fe7a73b56ee178ec
+15218bab55236d62fb8b911c2ae1ee05dde1ee60
+900180ff2aa70e7d857403412753df6384553d26
+a9c43cbeb0542cf6538fe8025edc8863d2526c68
+d7d8f0c9b7d56f554d5a5cf5759f30cc3d36771c
+d703e5d9ac82b8601b8f4bfe402567b5ce3ebbf9
+3905a12ad511ffe157cb893e7769f79335e64181
+73a933454b09ee48ffc873b0ee767e0229f2d609
+c2c91403aa9d95efa09843bffe83ace4d52d3446
+c90f480010097efa3fb7046abe7fac3c9b8b3975
+13e888d5624e8087ea63638de7f4375f5c13ac55
+19344e551c8c5e56e91de14253b4f08ca05f9e69
+b1b8f098bb1e2f0f08cf82805d7bd29d2931f63e
+3a3e025bbb2f3e49facac00e270d8afa5d31b962
+195116405307f7bd9575f9621fd93344304341d1
+31252094210748399f7e43e7b6149190990f4e8c
+357e549bf43126e371a1f85c393d2560597cb32d
+df1f8ab23f915420e9c48744decbc45375f180d3
+f96c2eedf6800b8fc31032a02caf0d2b469ba9ec
+73405f0505813ec1bd25f03f2825315f3520bcca
+7e2447536c35ae67e3737a031fa1ac57026475a0
+970d4c4854dbcc3b0bf9b16edf1d47eabf3be242
+3c73519e6b54d3559555ffac40072957041f62d4
+46d461676fc1fb16fd7dee027065441d9a8b87d5
+f11f64bb55240dcc1767a1ec823aecd3531f1d20
+038e91a424078c5d81cba6c820cd981f0be6086b
+157d6e98ba894cba5582caeb15b429ca0dcbf2d9
+2c768cf9d1bdb6d3d84f662a847966b69c898f59
+4fd0f29459ec3ea65625b943b147df85e5826cd9
+c7e90c64e580ce5f95147eb4e117b56b5cda254b
+cd4f2496b274b0d55b7c48388c2ec0365d9bc266
+68b5e288a29ebbcd65e6d0a8eed47702ee4e689c
+22abd4a7ed7b061364e002f1fe08857850a309ad
+4c3b38be6fda8ba32fe6f29226539e03bd0c55ce
+355e946ca8b8a5e4c17317446b12fc374399810a
+1fc5c0122fffdada1630febc1f2e42952cdc7e2e
+8db042e1faef7be24d62b9287fd3b9add7a1b4cb
+1cbea023ce354939ae9082a62810b46f38ab1cd8
+f5edf5b99d1bae09314b9680e58766a4e3c1bbc0
+58a5ef79958b58736603f47cf211494fe5819601
+8f2038bec169ae6d62885f522202d8171e3f5f5c
+5488e29e68684648b4d733e90c6e3188d3bd5bad
+84c88e813117db46c6ac68b16a7739018eb99e24
+789c3655197585ba8771ce68c0117cbdd41ea390
+0510404a3c0d337763e90e5315548043bac65b06
+2a665d7c6cab59ea8e3bb7fc65249ee947e51fec
+d53423de534d3b5e68a7644d4218d835a8bfe6ce
+73f2a3f332f23579a29e090f70825dcf84dcdbac
+f79ae7f27e750c97c139cdbdd7c3223b39ed1a70
+c84a75f7a4b274c5c133b1df3648a5a24ed9f687
+cdf8e5a49192b81bcd39d9f4e39aa4812b58b80c
+1180461f564674e373222fec3b4fe8c2861ea6a6
+150d93bd910597b85500e74b97b96e7eb4bce2f6
+ec3b819ffe3392bf193483fea94d4404c88966b1
+729fc8ffd38c02a9576640b56376c36b49edf52e
+2ee31128fbd86244d547e3ff66b802dda699210f
+f87f28c563ad602cba605e84bee95693b77b8840
+9e92c5fb59af58867acf5512e95138fc368f7dae
+76b1489042e1bb45909832f7064f9a5437b68b18
+66f5d86face564c095b3c95848f070f50fe4688a
+f9b2b3ec52b88dbd68b2f2c6b246bf07f632b40c
+14f689f05c4fae52ac8bb95762ff43b9f7f4e567
+5ca84af5f7a3f4533b353c43a332b552cb2fc5e4
+c5f33e9eb55201c41691e14fff0d45e32c989a42
+9f83cf471949164a6352cb9e3a201b8bb317b89a
+5532c7b06a2f02e9cafd6673d5099798c4144690
+0d28c20ab4f03b5d8579132048c060affc36c466
+cddce1dfd9d4d7f1fc49003aa211f018bf8fad2a
+169617e3672bac804a271c0aaff9cdbac7b4b45e
+fdebb28d6ae398ccba88f3e2e63ef6d7f10f62f4
+0651bfe384a8d5865d6cab808ca0ce803af93878
+de89eb007459fd5400cd344dddf240fc33fd0b65
+c6a14beb887170d8c901e522f2f4dce3bf0b9ed8
+13dd0647b3ee39fae1140f8eff2b15d7f63ee546
+9f89105c1462f2a80e620ada1b95c3d08a121c3e
+1ed6496751273cf472538779266dcc3dc9797192
+4e8dffa66fc7be8f864cb48cf26abcef4cc32379
+5543fce145ff28a1c424b730b376fd4e3cfa0956
+bd951a4a8574baac21b7e1f3a09d1265aa51850a
+3fd1c12fa880ee45b0ff7b794238a8894306a790
+830ec14bc9edbd2c6522ff46ed0acfe477e7e32a
+e68c3109a709e2b732d0945f860495de161754a2
+1e0f4fda735167ff6d27c76a67b8b4a4ab31aaf6
+c6c40dd0ff4420708c2e0f5a0e0dadde93eae336
+baf0c18ac24acb9ac3d1a7c0030ad5675eeb64d7
+8d30906e9f2f68024eb716be9f482de5cec5b302
+ec9fce551828795e1dace26a11f57f9aaf1af37a
+28fb918d7e9840a7118b7aa0b6151b496f1fc1f0
+b9e58c5b98f7c89054ed5c0a0226066ba9d93c8d
+0c5db457cdd3852182ce70b96cb376337b8ad7ad
+36a48168274cbb6f31c35777a74ee16c06e1a853
+07ef3ad40bb01bc7798b241c88fda2eaba7aad19
+02aa9f2ba871e9639891986a97618e0917955fc8
+5f776d3c74ad532f36ab75a71bcbece6a62c831d
+f31ea9eeea91106481e1b2d30026b601555b6699
+c3d7f6bac18fbf8041662fbdda4f04e3f3b25e3a
+6280c4bcf1195c011d7a7abb5bf689df11d66419
+45fc4ef9adaf514bbe21f496cdea8869a147c81b
+fa1160786e34c057cf1212efd59a72c3931eb2a3
+09b285cc7d7c8768917c7d4e5513e3e73d752b68
+a8da5db6094c887f1087162c5ddfddf601560523
+b6134a31d236c376193e969a2df65c8427d280a0
+793e0d19fef38f8a151622257b13edf6786e469e
+e40e6a17b4df5be46a2cafaa3fca5f4c3cec5001
+4d82e160cb874da6dbddc27af7dfd1036772b8f6
+745ee8e3e74dc0674dd8018999707f025a9634f5
+f507baf298549096f08dc33de22f7301e9799814
+bd7ebd663da867692f2316b94db73c42c0f9a5d1
+697f07726d209cac519b528018559f8957c56069
+2297b5172c0c1c83f2d78fc726fac0803be6eeb9
+91e3543f82039a446c5be8293d5a79ec767d1444
+e997169214440256b5b759f6e7e255a302838c97
+77d174ae14afbc6e212eb7d957b11a231a036d96
+3e81ee29892006f16d5f1f26d9d6b341a8958fb1
+59957e1d84f8fe8117d9697154c3951ba2959480
+96c6fa03962edb98a9b6aa7793be4ff54e79bfd5
+068a293fd6b4fcf216fb84ca982699095613af37
+b3b1804ffad1b7d274bc3f8f5aa11b15049ac030
+63e394c13a50de0d9f6cef55a8c91830200c3dac
+e7ed33eba96d590bbc7179fd26db707c910d1dc5
+6b2084340a988f4123e71c6e30817806ec4cf3a3
+da721d3f48f821faa90d1a4778d77b03fd3dcdeb
+a433cb8d56a4fcd50bfc74b0204c916e08c9d5e6
+067fae6fd778d5b1d6b6436aedc0d25db58334d1
+e34c192a5aef80c7e83c78c2372602830671ca5a
+861a44dc56a983262caebf909be96c62254930cf
+417ed493a824863e30922deda64b9729b1c6d6e7
+2df6a0d803ac21f0d20ae9fce0a970b35b3663ec
+44bedcfc59292d3ff6b36759b324812fcb779b2f
+c620f7e60c8ce4ddee8fc1072b2a161fee862545
+82ce5a39b422aec7572d9a773f85be8eaecb1618
+dc0ea6defad83a0569896a9c23f11f5052a48107
+e1c15f1da71a3aabdd43a8ad669d2a755f315c77
+c78ee1aaa5c499019948c9a3dfca3aaa2f897860
+e66d0d34c541c6588da3ae06c6aff7e7c9ef5745
+c24d513d46b3db5b4c53b36b7e43ce5fdfd5a2e5
+a75d0a4bf6d2e1a9c4026586cb707f254691eb3b
+41e98ebf4526e78d78ab16182b503f237e77fbd7
+2182dce8c27c33f9452e7c910f59750d1e58b1e3
+6b7aa9fdbdd0160ec29b6b3b591169c627fd0f01
+b39470063e41ee5773f47de325a845666d0721ce
+c7941bdc8822ae1842d2a2f42924f31d2d37e864
+fad6e836009429e88c788ab7e7a679d422d8cae1
+a478917ebcf70c5dd6f56c7cd139832108696189
+4101b1ae3b17e229c1a80d9c302b74d215d98f04
+b051ec4e69a99e26d6a6e5d7a393014f841eed6a
+5298ce551a104605b7d5d9872387f3eb704fe5e9
+b14a12bed26d53eaccd1a2c172ca4a38773e1d45
+4ae0790397d05a758013e0496ba2c2b23363361f
+431f01bf3aea6f8baaf06669172561a3ec9e82db
+12476263aa193c7e921ad4b183fc648bd73d2a1e
+8e937050fb12a62e99b0cf685578213552774cc4
+b85a487787454f6dac84be59f905b8c929f0ee94
+dbb2116e0f03fe6d84d2158d67ddd02761938bda
+57186ad57242ad0bcd737c4ca4ecb7c063979a95
+cdb4a295593cb3ad424b4ab86d74154d7bbb97bd
+8e9e1ae0edb776f0a490005b838f8ef82b368be7
+73f8f21a69a03cdc2b1031bca214a6b84f4c867c
+b913c6d878ac5cf570e6f8ba9b5ff022ed601a8b
+b98879530fd51f328441d33c64c6c5f311097e15
+325b21b5370a0c179b40fd596b9daea00b3615c3
+6e722a5c5393dda24172de6f8e08138bcbfb10b8
+44b396caab82c97a6270eb7391d6f96502c9fcf9
+4e6ed6d22079b68551bbb83e5dd797517796a438
+b611fe79daa20893683475cc459dff98b2d4892b
+017d40f9b23f4a4379c74ceeb89ce7b4bccb7460
+a31b0a7fc7190218136d6ff6ffb3ee6af3244135
+861bd42abb90a61ed757728e1fef7cee2d6aa081
+6e9ff586de744d166a9f6f213b609e7386692472
+a790ca7384982e872092766c036d6faa86bff71a
+13485c50ca4dfd885d516154421bdb23cb034230
+c5471e696f3166942a245e77796bcddafe6a607c
+600308daf62d0d651fcfc874110e7bd4f5de648a
+bada607744ec7f37ba9d05c09bb8f41e7fc3d06a
+d3b230b209fd7c3f4a39db965b239ab600fac1fe
+6d730b7ae0b662b1f987101e8ccf9c1828554d69
+f0757668fcd3f8d1f2fe83ce9f0e2355b6be75f9
+40819d9a5631a184a17d38e36240d1171a6fc923
+8a6847ca68ec998df0543c4b5bd5c709c05d5f12
+d8eb0646ae1360b5b984ba7d99bc64e00dd67016
+761bf1cc1e2b86437e71c9a106fc9c341097c3bd
+3b620d960d29fa7719f95cf945163b04e43d2dad
+6be8590f72c2ef158202486e75f273d8598be6bc
+d7f22a15d66139efd65bae28ba780b0bf8d1a914
+e1ebbf612cf9d49cb08d0e0770ac1678ce1436ab
+4db9912f07ce63e4519053f52dbe521ec95c0fba
+b9fd4f4760ef65934b5d38e8b7c0eb2f77822861
+0e0178ecfacd553526afd221734607971b6911f1
+8cd4823a8ac9f846930408ad1759da4496384f9d
+e96cf22a972cc3185739ef1c1ce74a978ab71d11
+a9d63829aa54049801d37429b597eb04c9e1412d
+2519d617e18fa35974e20d10414f1262013501bb
+d02fc8d8483903871d9f65261b32c6acd2e4362d
+569456505d5c97934344d4f989a08fdcdb522de9
+f56d4c60ffb8df8fc1516d32a0512def0b6f8296
+745e899452ec746d3ffbb7b082995b7939a85387
+8c11f9ca2433bf9381840696218c245ec700666c
+bc2a868d1ba12b485a6eac460cefee67bd9ee899
+e628b072d054d982ecbbd7aa7fec628e0d9ee8d6
+e3390afd65e721dd8ef228f48fd4244228de2986
+35102507bd653296eaaa5e7d475405cc1feafbe3
+e2e5342f92148238391665fba101b1ca7dad5582
+621f4743f0165c6ca3f1571773867d2e0da67961
+5f558819695a49bbb92d5d1e07b9f12072874024
+eb45e9da84875e2d1325b78157d2f9e96374bdae
+bc0ab7e4f643e779cf9554f03e567d4f4708bd4d
+fd55e896d6df035cba49a20e26ed6ddd2d7b6024
+dcb9d95840c9a0514f8fd0a7b3b17cc228950c7e
+0bedc3d7a01f9819171c0b664e16900d9965c3ae
+94f6e372fd90e96cfd9a393a5952aa850485de66
+0b889a9cf37997c58a9f8979850da1f4bc84de9b
+b70ec5facdea7fc681c2a10dfb14ca0d8fed6f1d
+03e0192fb34134f25784a2b14791fbfdf69461bf
+9266cc52df3725107edf513aea4a02c131aa153c
+0820a412fdc9941567d86cba02793ca6a6378275
+f1a72254956f63393f6039a7d5da5fca943fcd2c
+abeb9e16d924c1a87c5b525ed12c43031ef1cb2f
+d5813fad322c97bc31d7dd37f838c7442aa68f35
+428b26fdca0ba98a3a01e89629bdb778dec9e8ab
+19ff672db65a7ee25ee0d48baa3f9bbf2d145ecd
+d1eb6283ece7d9c814b0d3d5223207b905d3d720
+9ba934b83a40d26ebc5e8d7304ff29c32541e82d
+9b600cbf0209ad6079d00dd5d6a5270d858d5929
+0f22868d790bfab8a41894fc7eca161256ab6854
+fba092070b6e03432f6d47154f5ae4734e935a05
+11b1bf011fc24c2ca6dd8c81206c7338ac2b2915
+d93c82b17d7416e4c57ed036d6b75a323859d837
+27f762e8d3f1ed8bd0254800c121d0f16e914c2e
+e252d9d270330072e9e5e91257e90f255e7e968d
+e55c3c30785eb50b5dc36f9568e6b6ae39e6de11
+63491807090d814bd7ffccfe44cb05795830eb3b
+8111dbaeb71c53132229c4064c34247746a3769e
+8fe37ca0d79dd1f8132e9add06aa206d371964e3
+eb32fae4665b9f11ffd06a342e763b9d212e1353
+4e923698ee5566143fc6d32fdcc6fb46fcda2d23
+2e3910e29142382f9bbd1705ab9c605d1937a1ae
+533cc5f884885f771d3f6df4164fbfa29bae0e6d
+3fea0404fc58822cfc60d4f10ca404e3223f82a4
+733404a081eda804707c3dde1d6b8161e7a34b3d
+d2be0ea2923344abed57aa21f13dc816d4537eda
+7884465bb9da51c8b6e95a1cbc9888ed696ff68d
+6e63e5a03bfbba52dc3b4f504e6bc41951f56707
+44a9d3ed75c44e817a6e4b56e30be06a15f453c9
+91e12aff0f988bf414e64b97a8c20b9699440309
+008119e510f6a7f8714e63d2ec33ca7cd7776ea2
+27822b01ad020374ff6169428649fd667abf7f8b
+0c972fb8903c656cb7e750b1d5c1ea1f26bd8c50
+3d8f3e1fae697a905e87250aa5c0ae1f6c60ad66
+744421b6f1d3aa30c7558570da8aa1d52f11d39d
+ac017796cd3a5558dd78f73ecb82a6b961d8a3ec
+e11f534a2fd666ecd841f657faf0751d5fe02034
+eca5d275376911916c3e018c2d163cb8eb914263
+a3144ddce360b6ac1b55fc27d19a318be1f224c4
+84fc7d68bf3a309b3687da768f0dc206e647e653
+fd5132bf8e99230a9074ce9bb3d950cd26b3d25b
+720ceb5e566d26803db85af3ef69fc4fa14d355e
+e97f338a79e2248afd3a2b9077d8ac1c334cdf38
+0173ccf8d04014bcc4cc53df4d6574540f4231e4
+52da09b8812d96c14d3e57a77784d56e5749a8ae
+5169648c7429788c777947e21527e121d35aebe9
+41c8c94cdd1c646296946a00dc72dff8fcb6556f
+9b341f77b72b55674a030ad0209ac297e41c5570
+6aacd7b9b8fc571e930d18da63efc8be46e31bdc
+9875e5d15c0750b6ee4c41b0e1321e1dc0bb7810
+fd60909d92b0e124957aa0783ea03471c73fd732
+2f299011d707ffd8502e5a597f38f0d25ab3099b
+6c10423816abd3b0f327863c9b8fcf55cd6265bf
+14cc60568455ac2210f00ccb238ae41ddb473fcb
+74cf0e9a42bf241d3f76f25aaed46e4b6550d842
+9a0eacdab0398ced7d729f5c7a9b173eada2dcaf
+3057f2e5ac8cd11cd018780c062da7c2bb11d2f7
+dab224a6b259d9d7e16af4cf7e2718af8ba4a74c
+fe6dc165cde8c826a3935b536c8cfd1c10ba7d62
+7f3572bca7fd48b66649d761a054412b8369deba
+2ea30dde468795a3ccb307343cd50eb7041f5ee3
+5d4099ededa31d823a355d4ef0e53bed6b833539
+69eb5257143b2de63c8c7471216ba6f025b6d7ef
+e4c7387b32e314cca7e0ee2b1df197340272fad1
+01f14dd38700098d97f933008327c8456c75af34
+94040e25d5aacae0e55c3e9a91fe24d7daaaaaca
+cd64f093886bf092b8d88c75ccd2e2f9118d3ba9
+ceb96f9512f80188fafc61ec8d8d61c93d51a5c2
+9a4e9bf98bd371cee2b69ef62a1189c24cd8baa4
+dd861f56b65404a625538978d50819924f384a60
+b2960c129e39d30f446d27e38f726975bef7b4f0
+8351c6b1293bb0cc4a2e1235995c16433c84c463
+008ba61116504d01558fe8afea0d5b3e90944b76
+cce20d2824a877ffed6a912e3f22d7db3d8e5043
+5e02e12edf58e1dfe37ed770fb32171e64993a81
+7966a56b3a3c9c9ac6db5b9355ba5e96558ea7b6
+5dea2f86730665894cf03f2b1fac98c1217a9fb4
+451a4d8118d2c9c746c687efceaacac799e67ad9
+059dfb5adcde569a19a9260c2ff85c7b47f8c516
+da7449db2898c567fcfb40c595c0c21536c901b8
+db97ce996b09b15049a9f818ce27a680e585bd11
+e1f95b9a8fe2394e1cfb41fe83f130bdb68fe6b4
+fc2c03e29a331cafc8b08abd5eade336904f40dc
+385b11a95469f7477bdcf5b9c743982c4a866c65
+d7e31d19b9ed766048ccf9129723ebe36b4842dc
+9c9af56fb29f510ef75221a39964c128448526bd
+83e3c642af5648aaaa119cce34dfef6ef3c560bc
+a831fc506ca30a11c9d9b33c9cb2c43f6f01a446
+62c5ebf183a0cc2332f04c1ee3323005a9878438
+6bb31edda343bbbc4410e2f780c432129e610b47
+846ef94e8af8f09340a740d11c93157c81079bc0
+47aec581139d8a3ab4f2969b481868c1485e2ac6
+e3f68d2cd84e15063c4f73c8420a444f9fb64a7a
+3db1240470361a7314ea096f63c0fde74810caba
+ae951371c666cc605ef69b5ca3f5f31d0cd30298
+8ec035e739f01aeaa09742a92154f02ab3dbfe93
+4737a65f7c1e125ba37ef35acbc6e99c4db2bed6
+7005d4cae81a16a5a860fcd3c259d6ec07597072
+d98807cb107ad2e9bf95138ee4bfb566bf75cb50
+1e8cbd548f12e1ec861f3aed5fa9f080cf2782c4
+25c2b2cad9cf873edc80747cd2df5874034282aa
+676749cf8f76eadb469289b1d918cb5e485cd56b
+8cba76ab8a5034ee21e95a99196f257b7e527b49
+0151aa85f5a178da21ddf7d5e81398fff87604dc
+f881500552171b5a8a8c3ec7a2dc06e493a1ebbe
+8d39edf2ae13ed33d0529164d4e172bd4d060d7a
+b5c3f29c81e524e860e5f9ebefdc573f83fc600d
+b686bc7a882e461987ffb7bf1a25bdc6f82ccdd3
+ebc1f42a059e7863adb57890562878f652922b56
+b30835cea58d0b827cb56aaf9e4d5f6e673a1bf1
+a2cf1028df49cbf53c57d0f599083fec59cc38b7
+6efa045dbdfb4272f075255411f54fe436c31b8a
+0c3f085a4044e9231287c11e34504624b04ee7cf
+b8e628fdc2a7627283e0601ebfe8e978e91dfc00
+d84e30103d59d6bace53223fc0d5787f03d7f028
+2e0e70d0466bde79d134a215a399b20c2a9d0981
+142de640101e2bee71fe2dc98e567d688c7e3aa7
+8b02a5e91092f7363443a1cf96933dc445f0ce51
+753c065260b1659c0d8d247b62f6b0fbe986c7b2
+1113b6978475c9941be9b140e8cd6bc267469657
+0a01d10b21c039484410c7898250afc4079db28d
+b9bd23fa584a8f1900ada4addb96eeb750ef0a68
+5ecc9b675c4cc5c1bdcd8f84e1a52457ad30144d
+d91b0a31122b251998915b4eb274350fd42a841e
+a829cb9c850cc75546547aa95fa3ca6100ce16f7
+4b9bba5d1063d986be6463e4c5740eb18befc7b6
+ffb2f17926143e242efc18b32ee0c630b5447687
+3feb18fbff52f17a541abb1ebbb4894beec18d55
+4acbde9bdb24bd802ba5bb0ebe19d71c8d753240
+c9dba689c67ad7b16c8f6b1bf1bd382369fdec4e
+ff956cafd71e4787e9ef7b64725142fe8838a65a
+e2c090f1ca171b51d08e6ecbb74b27410bdfa7eb
+73aa4812a2effb88bb64a42f93713a54a88e1ccb
+8e0e0c69b0adb9a65098b18a7b96d6ed3a43940a
+5dc8620cb17c3e606b635f8f95ecebdd66af04ee
+18f8afd6fc87b3731145f61818f23b4b766da703
+0d2d0bd0680557dc28f4f7b23562495cdbb3afc0
+94da53667213590ad9767b335a9f2e51fe1e2c5d
+c6cb97a42dcea5461a2931b097ddfd53b9cc5870
+62a3d5192232ed847f3c7810344c43607a361e68
+aa6992567e763a0b081e6bce753cc42bc287e9d3
+1d67358d33250d456040091d8b29083b1b47d9bb
+65d399a4ac7dc36df20b8b2bc773bbc6fa67f43b
+acf7ea014fd1b7eb351dc6946b199ad2cc98f845
+7e4dcbb7f0fc2b051e33b555c4fdc67796dbbab9
+a07916245a9c21f3874a7b8c898638ca3b65df42
+bb7368d9b07b02aecfbca6d01788a7327743ffed
+60454c29275aac27c450323f0141d60ea8202842
+c4d0ff10c85ca4c12ddfda1830cee475408205d5
+a5da3671524fb761552a4eb5c1e27dd433f80fe4
+43142e711f392ae1bcdade749dbaa9dd98664228
+7aa0bdd118c78d8929e737392457d14f87d625ae
+be921331245c4e04ef9f0ff7e359907e2d101cac
+d6f654de1b8c27f84e34fbff12aadffb30342465
+fef2680b335ffd861021ceff2a2637f5a360f037
+79de53d3b87469e21d510ed6ddb33d809c05a3f6
+475b10017d25db725e73eef11ca789ad7dfcf4ac
+d14f3734dc27ecccfdb4683cf7ef3334a5a70b3f
+f0c394dd6a109b97ba4a9ab16cc71b789d9ee38b
+a57cd5c8278e1fd6fae6f02947c13880be4f3b62
+83c6e4b636f3bf115955b6eeb3f91a5689e7f00b
+b881752a8cc16f49ca605bf6a35af106e7e19c9a
+8362e4bcc30e73460ae1b9731bc545fd2b12d8f0
+b01216229149bed7c110221551353b54ff8e4704
+10ad0e68785b27bab975868b83bc463b9c9c9153
+7a66612abaa223ef0410fae66727a8abac3add03
+8a7bdba957536b078f0421faf5dfaf8d65ff5add
+defd6d03526345a410437eda15cbd067124f9c2c
+f7e6c29aa4d1f7a607e0c87ea20105afeee0372a
+751363e461257a4036a8f2aa740195401883c1ea
+a8d66b5855eda5abf699ebf9c6dd721928007fb8
+35ca716114bdf87a89857f2d633be3f4b13cbc70
+cf319abfba8fc1b33de4c6a6f99e21864cc72563
+4fd36e634e762ff2f94e9d66f24ceabe164f9e26
+d0364113a1b57ed5017dbea6126b0cc5a5c2886d
+9b3d7bf551d20acae4ee943a86c3cf898b6280ba
+b35351d566efdde005747503c7f121d49e864848
+57b1dc2b20f2e67c3313f0c6127b05041d125fb4
+fadcdf4c98e9167f8f06a45dafa08d3acce7a741
+3bcfcb7717bfc0e50c5b8f5c7beaed9f3ddf5478
+b8388b7b5973dd3e84902c25c5378f9a412d6147
+814f07ea363eb0464380ccfce7b4cf5209f1dcb2
+b33315c8551bede3fb867efb3fdb1134cdff5115
+c7bade1e7cc239e8fceb2c0b06f880e60eb8ebec
+bb193f4f0f5b1b8bdc9cc72967f8fa6387faf7c4
+b727e8d9f4a4987cbad41c75c630cfdb445c37a0
+a2103d7fe328871d8231f8e07ba5dc9182f637b3
+e36d269d16660db5bba028746564b5699721def5
+35f9c486cc26bdff903241f4ab2b1dac2536059f
+cd5314af7e8e120bceda896a3c17daa8eeedd528
+200e09df8f0f7b94eb8941136482cf7c60fffb0d
+17a618f241a6236c93af5ba2e09238369fc7d784
+15aeb2bb0401d428cb7058e1d6554e20369ed352
+40b0a406cc23467af8bb63d9a62378fa871e2031
+7abd7f4cb237ef33b9e019f4529b6fb05b84284f
+ac614b7506e820457417c3ea15ba99fbc8146155
+8afd5a714da3f45389e0e4edeb64f49576c57c76
+77d10571047d8b4153180e7a89d5c9aae6a84060
+35479ce1706725f73bfe99428c43e8fe2e3f9157
+360a0864ece712571d3df95e86251d6883bcdf7d
+b5cd910848f592e33efb6de3226c07ae545a2aad
+f5a9c28ca029ec5d1c5d3c594afa09374adf04e5
+b9fce5928a1c5056f66706b67c01cd564e6c0a90
+e5a2250e35706127304cd5ed86b81575f2636b5b
+f30cffe4cef93aa190bcb1caf407ca0767107d06
+45535f6e0af6785676531c81b4a2a3c480a98e70
+740bd201b23beded9ade92a93301cddc67c4d106
+70460e9e601171276dd6844cd6addd8db5eb2465
+44dff2c35acb4736b183cef9e46155386f579716
+46ea31f673bc9365fcca558f15c862ef6a899018
+34556caf76c2422a76be3d1cecd223fcf435d93c
+fea67ed9483b5cc76dc55eb4dd6f52baf445394d
+31b1897ece6222826f379c1aebda891384b4b63a
+80dcf3713b85b78979d4eb443fce9e992675b5c0
+11993c742658321c0c5c200f48231583216d636c
+7b5a089ed3007252e61df0aee3fc17c14d051745
+890881c9a552c22f4be01dee16ee902c88f6700f
+401ba79da09dded82a73996c8e0609a87cbd728b
+e06313f41971de730085dcddf640a4549fc54fc3
+054d52e86a954a615ed1f5add7f9d6842737d965
+d8a60982c456a9cae3de745a37dc3f5985814f7f
+2b39f575a510cf581aa828df494e633cc76fafa6
+e11d353191175b329b3c9f9af7fa33e3ef9f837d
+32ac2659ce98765aaae9c10cc7216d1f1faf155e
+5f7f801227868c7abcce7e58dee3eff855011955
+a013eaf0fe38d8689e27278bddd4ebf87ac5476b
+401b3f3d2d96fa785c5321bb64c97cfb17c509e3
+1fa4fd4321fa708b3db5cfb514e2192b00672aff
+77976b24ff839c59c3b20d80cb28351ccb5e59a8
+09b76d2966e2370a78ed37a31c2f7c23d08609c3
+7000b24511618a21d40b39ee213d397e1d29497d
+c2a6adfcd18c0d95dbed6ea62ac9c9a912d18123
+6ba3609953d5c46a76ca1d0d3d83018be61454e6
+3dff6074fe205e36fae219f277ef87aab097e236
+1cdc8437fa6c621d96c4dfa5f6370c8fdb9cbc3d
+d471720bc8f7ce7109276b49dd9c76b6163007d9
+a67b1bdd027629dfc38601b21dc564272e28712c
+20125a6d37d5c1614ffe1de94ca064095968e7f0
+2b642751ef86265a1c953186810e118740f8bd2d
+e562c1d74e2b6744572184e66a0673e55f9ba0b8
+ba9687b5d746dda28d4a19c5c96d0679d7c77b15
+f39d7d293c3e342b4f447bb440a9b6f72d2d20cc
+95750ad9e700efd15d137963ba0dc443e6c9b6b0
+0f76d8445048dc0bfcaf05e30b61b338a08f0e48
+1a9a4c61d6a371d9e95eaef44fa2452d17a09d22
+912b41aad5983d9735379d322eae8f6d40d8bdca
+eea0b559472874ff48c34f16bb805108967e6489
+ad4e7ba4032e6b1c047230b3144848dbcf66a127
+b6d93107393dee6eebb05376a67f2e4dfcb44311
diff --git a/test/fixtures/rev_list_delta_a b/test/fixtures/rev_list_delta_a
new file mode 100644
index 00000000..023c5515
--- /dev/null
+++ b/test/fixtures/rev_list_delta_a
@@ -0,0 +1,8 @@
+e34590b7a2d186b3bb9a1170d02d52b36c791c78
+8977833d74f8681aa0d9a5e84b0dd3d81519774d
+6f5561530cb3a94e4c86454e84732197325be172
+ee419e04a961543444be6db66aef52e6e37936d6
+d845de9d438e1a249a0c2fcb778e8ea3b7e06cef
+0bba4a6c10060405a94d52533af2f9bdacd4f29c
+77711c0722964ead965e0ba2ee9ed4a03cb3d292
+501d23cac6dd911511f15d091ee031a15b90ebde
diff --git a/test/fixtures/rev_list_delta_b b/test/fixtures/rev_list_delta_b
new file mode 100644
index 00000000..aea7187f
--- /dev/null
+++ b/test/fixtures/rev_list_delta_b
@@ -0,0 +1,11 @@
+4c8124ffcf4039d292442eeccabdeca5af5c5017
+634396b2f541a9f2d58b00be1a07f0c358b999b3
+ab25fd8483882c3bda8a458ad2965d2248654335
+e34590b7a2d186b3bb9a1170d02d52b36c791c78
+8977833d74f8681aa0d9a5e84b0dd3d81519774d
+6f5561530cb3a94e4c86454e84732197325be172
+ee419e04a961543444be6db66aef52e6e37936d6
+d845de9d438e1a249a0c2fcb778e8ea3b7e06cef
+0bba4a6c10060405a94d52533af2f9bdacd4f29c
+77711c0722964ead965e0ba2ee9ed4a03cb3d292
+501d23cac6dd911511f15d091ee031a15b90ebde
diff --git a/test/fixtures/rev_list_single b/test/fixtures/rev_list_single
new file mode 100644
index 00000000..d8c6431e
--- /dev/null
+++ b/test/fixtures/rev_list_single
@@ -0,0 +1,7 @@
+commit 4c8124ffcf4039d292442eeccabdeca5af5c5017
+tree 672eca9b7f9e09c22dcb128c283e8c3c8d7697a4
+parent 634396b2f541a9f2d58b00be1a07f0c358b999b3
+author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
+committer Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
+
+ implement Grit#heads
diff --git a/test/fixtures/rev_parse b/test/fixtures/rev_parse
new file mode 100644
index 00000000..a639d89e
--- /dev/null
+++ b/test/fixtures/rev_parse
@@ -0,0 +1 @@
+80f136f
diff --git a/test/fixtures/show_empty_commit b/test/fixtures/show_empty_commit
new file mode 100644
index 00000000..ea25e32a
--- /dev/null
+++ b/test/fixtures/show_empty_commit
@@ -0,0 +1,6 @@
+commit 1e3824339762bd48316fe87bfafc853732d43264
+tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+author Tom Preston-Werner <tom@mojombo.com> 1157392833 +0000
+committer Tom Preston-Werner <tom@mojombo.com> 1157392833 +0000
+
+ initial directory structure
diff --git a/test/git/__init__.py b/test/git/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/git/__init__.py
diff --git a/test/git/test_actor.py b/test/git/test_actor.py
new file mode 100644
index 00000000..f045926d
--- /dev/null
+++ b/test/git/test_actor.py
@@ -0,0 +1,23 @@
+import os
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestActor(object):
+ def test_from_string_should_separate_name_and_email(self):
+ a = Actor.from_string("Michael Trier <mtrier@example.com>")
+ assert_equal("Michael Trier", a.name)
+ assert_equal("mtrier@example.com", a.email)
+
+ def test_from_string_should_handle_just_name(self):
+ a = Actor.from_string("Michael Trier")
+ assert_equal("Michael Trier", a.name)
+ assert_equal(None, a.email)
+
+ def test_should_display_representation(self):
+ a = Actor.from_string("Michael Trier <mtrier@example.com>")
+ assert_equal('<GitPython.Actor "Michael Trier <mtrier@example.com>">', repr(a))
+
+ def test_str_should_alias_name(self):
+ a = Actor.from_string("Michael Trier <mtrier@example.com>")
+ assert_equal(a.name, str(a)) \ No newline at end of file
diff --git a/test/git/test_blob.py b/test/git/test_blob.py
new file mode 100644
index 00000000..7091136b
--- /dev/null
+++ b/test/git/test_blob.py
@@ -0,0 +1,67 @@
+import time
+from mock import *
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestBlob(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+
+ @patch(Git, 'method_missing')
+ def test_should_return_blob_contents(self, git):
+ git.return_value = fixture('cat_file_blob')
+ blob = Blob(self.repo, **{'id': 'abc'})
+ assert_equal("Hello world", blob.data)
+ assert_true(git.called)
+ assert_equal(git.call_args, (('cat_file', 'abc'), {'p': True}))
+
+ @patch(Git, 'method_missing')
+ def test_should_cache_data(self, git):
+ git.return_value = fixture('cat_file_blob')
+ blob = Blob(self.repo, **{'id': 'abc'})
+ blob.data
+ blob.data
+ assert_true(git.called)
+ assert_equal(git.call_count, 1)
+ assert_equal(git.call_args, (('cat_file', 'abc'), {'p': True}))
+
+ @patch(Git, 'method_missing')
+ def test_should_return_file_size(self, git):
+ git.return_value = fixture('cat_file_blob_size')
+ blob = Blob(self.repo, **{'id': 'abc'})
+ assert_equal(11, len(blob))
+ assert_true(git.called)
+ assert_equal(git.call_args, (('cat_file', 'abc'), {'s': True}))
+
+ def test_mime_type_should_return_mime_type_for_known_types(self):
+ blob = Blob(self.repo, **{'id': 'abc', 'name': 'foo.png'})
+ assert_equal("image/png", blob.mime_type)
+
+ def test_mime_type_should_return_text_plain_for_unknown_types(self):
+ blob = Blob(self.repo, **{'id': 'abc'})
+ assert_equal("text/plain", blob.mime_type)
+
+ @patch(Git, 'method_missing')
+ def test_should_display_blame_information(self, git):
+ git.return_value = fixture('blame')
+ b = Blob.blame(self.repo, 'master', 'lib/git.py')
+ assert_equal(13, len(b))
+ # assert_equal(25, reduce(lambda acc, x: acc + len(x[-1]), b))
+ assert_equal(hash(b[0][0]), hash(b[9][0]))
+ c = b[0][0]
+ assert_true(git.called)
+ assert_equal(git.call_args, (('blame', 'master', '--', 'lib/git.py'), {'p': True}))
+
+ assert_equal('634396b2f541a9f2d58b00be1a07f0c358b999b3', c.id)
+ assert_equal('Tom Preston-Werner', c.author.name)
+ assert_equal('tom@mojombo.com', c.author.email)
+ assert_equal(time.gmtime(1191997100), c.authored_date)
+ assert_equal('Tom Preston-Werner', c.committer.name)
+ assert_equal('tom@mojombo.com', c.committer.email)
+ assert_equal(time.gmtime(1191997100), c.committed_date)
+ assert_equal('initial grit setup', c.message)
+
+ def test_should_return_appropriate_representation(self):
+ blob = Blob(self.repo, **{'id': 'abc'})
+ assert_equal('<GitPython.Blob "abc">', repr(blob))
diff --git a/test/git/test_commit.py b/test/git/test_commit.py
new file mode 100644
index 00000000..d47c37ff
--- /dev/null
+++ b/test/git/test_commit.py
@@ -0,0 +1,191 @@
+from mock import *
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestCommit(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+
+ @patch(Git, 'method_missing')
+ def test_bake(self, git):
+ git.return_value = fixture('rev_list_single')
+
+ commit = Commit(self.repo, **{'id': '4c8124ffcf4039d292442eeccabdeca5af5c5017'})
+ commit.author # bake
+
+ assert_equal("Tom Preston-Werner", commit.author.name)
+ assert_equal("tom@mojombo.com", commit.author.email)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('rev_list', '4c8124ffcf4039d292442eeccabdeca5af5c5017'), {'pretty': 'raw', 'max_count': 1}))
+
+ @patch(Git, 'method_missing')
+ def test_id_abbrev(self, git):
+ git.return_value = fixture('rev_list_commit_idabbrev')
+ assert_equal('80f136f', self.repo.commit('80f136f500dfdb8c3e8abf4ae716f875f0a1b57f').id_abbrev)
+
+ @patch(Git, 'method_missing')
+ def test_diff(self, git):
+ git.return_value = fixture('diff_p')
+
+ diffs = Commit.diff(self.repo, 'master')
+
+ assert_equal(15, len(diffs))
+
+ assert_equal('.gitignore', diffs[0].a_path)
+ assert_equal('.gitignore', diffs[0].b_path)
+ assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diffs[0].a_commit.id)
+ assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diffs[0].b_commit.id)
+ assert_equal('100644', diffs[0].b_mode)
+ assert_equal(False, diffs[0].new_file)
+ assert_equal(False, diffs[0].deleted_file)
+ assert_equal("--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1,2 @@\n coverage\n+pkg", diffs[0].diff)
+
+ assert_equal('lib/grit/actor.rb', diffs[5].a_path)
+ assert_equal(None, diffs[5].a_commit)
+ assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diffs[5].b_commit.id)
+ assert_equal(True, diffs[5].new_file)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', 'master'), {'full_index': True}))
+
+ @patch(Git, 'method_missing')
+ def test_diff_with_two_commits(self, git):
+ git.return_value = fixture('diff_2')
+
+ diffs = Commit.diff(self.repo, '59ddc32', '13d27d5')
+
+ assert_equal(3, len(diffs))
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', '59ddc32', '13d27d5', '--', 'master'), {'full_index': True}))
+
+ @patch(Git, 'method_missing')
+ def test_diff_with_files(self, git):
+ git.return_value = fixture('diff_f')
+
+ diffs = Commit.diff(self.repo, '59ddc32', ['lib'])
+
+ assert_equal(1, len(diffs))
+ assert_equal('lib/grit/diff.rb', diffs[0].a_path)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', '59ddc32', '--', 'lib'), {'full_index': True}))
+
+ @patch(Git, 'method_missing')
+ def test_diff_with_two_commits_and_files(self, git):
+ git.return_value = fixture('diff_2f')
+
+ diffs = Commit.diff(self.repo, '59ddc32', '13d27d5', ['lib'])
+
+ assert_equal(1, len(diffs))
+ assert_equal('lib/grit/commit.rb', diffs[0].a_path)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', '59ddc32', '13d27d5', '--', 'lib'), {'full_index': True}))
+
+ @patch(Git, 'method_missing')
+ def test_diffs(self, git):
+ git.return_value = fixture('diff_p')
+
+ commit = Commit(self.repo, id='91169e1f5fa4de2eaea3f176461f5dc784796769', parents=['038af8c329ef7c1bae4568b98bd5c58510465493'])
+ diffs = commit.diffs
+
+ assert_equal(15, len(diffs))
+
+ assert_equal('.gitignore', diffs[0].a_path)
+ assert_equal('.gitignore', diffs[0].b_path)
+ assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diffs[0].a_commit.id)
+ assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diffs[0].b_commit.id)
+ assert_equal('100644', diffs[0].b_mode)
+ assert_equal(False, diffs[0].new_file)
+ assert_equal(False, diffs[0].deleted_file)
+ assert_equal("--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1,2 @@\n coverage\n+pkg", diffs[0].diff)
+
+ assert_equal('lib/grit/actor.rb', diffs[5].a_path)
+ assert_equal(None, diffs[5].a_commit)
+ assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diffs[5].b_commit.id)
+ assert_equal(True, diffs[5].new_file)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', '038af8c329ef7c1bae4568b98bd5c58510465493',
+ '91169e1f5fa4de2eaea3f176461f5dc784796769',
+ '--', '59ddc32', '13d27d5', '--', 'master'), {'full_index': True}))
+
+ @patch(Git, 'method_missing')
+ def test_diffs_on_initial_import(self, git):
+ git.return_value = fixture('diff_i')
+
+ commit = Commit(self.repo, id='634396b2f541a9f2d58b00be1a07f0c358b999b3')
+ commit.__bake_it__()
+ diffs = commit.diffs
+
+ assert_equal(10, len(diffs))
+
+ assert_equal('History.txt', diffs[0].a_path)
+ assert_equal('History.txt', diffs[0].b_path)
+ assert_equal(None, diffs[0].a_commit)
+ assert_equal(None, diffs[0].b_mode)
+ assert_equal('81d2c27608b352814cbe979a6acd678d30219678', diffs[0].b_commit.id)
+ assert_equal(True, diffs[0].new_file)
+ assert_equal(False, diffs[0].deleted_file)
+ assert_equal("--- /dev/null\n+++ b/History.txt\n@@ -0,0 +1,5 @@\n+== 1.0.0 / 2007-10-09\n+\n+* 1 major enhancement\n+ * Birthday!\n+", diffs[0].diff)
+
+ assert_equal('lib/grit.rb', diffs[5].a_path)
+ assert_equal(None, diffs[5].a_commit)
+ assert_equal('32cec87d1e78946a827ddf6a8776be4d81dcf1d1', diffs[5].b_commit.id)
+ assert_equal(True, diffs[5].new_file)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('show', '634396b2f541a9f2d58b00be1a07f0c358b999b3'), {'full_index': True, 'pretty': 'raw'}))
+
+ @patch(Git, 'method_missing')
+ def test_diffs_on_initial_import_with_empty_commit(self, git):
+ git.return_value = fixture('show_empty_commit')
+
+ commit = Commit(self.repo, id='634396b2f541a9f2d58b00be1a07f0c358b999b3')
+ diffs = commit.diffs
+
+ assert_equal([], diffs)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('show', '634396b2f541a9f2d58b00be1a07f0c358b999b3'), {'full_index': True, 'pretty': 'raw'}))
+
+ @patch(Git, 'method_missing')
+ def test_diffs_with_mode_only_change(self, git):
+ git.return_value = fixture('diff_mode_only')
+
+ commit = Commit(self.repo, id='91169e1f5fa4de2eaea3f176461f5dc784796769')
+ commit.__bake_it__()
+ diffs = commit.diffs
+
+ assert_equal(23, len(diffs))
+ assert_equal('100644', diffs[0].a_mode)
+ assert_equal('100755', diffs[0].b_mode)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('show', '91169e1f5fa4de2eaea3f176461f5dc784796769'), {'full_index': True, 'pretty': 'raw'}))
+
+ @patch(Git, 'method_missing')
+ def test_stats(self, git):
+ git.return_value = fixture('diff_numstat')
+
+ commit = Commit(self.repo, id='634396b2f541a9f2d58b00be1a07f0c358b999b3')
+ commit.__bake_it__()
+ stats = commit.stats
+
+ keys = stats.files.keys()
+ keys.sort()
+ assert_equal(["a.txt", "b.txt"], keys)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', '634396b2f541a9f2d58b00be1a07f0c358b999b3'), {'numstat': True}))
+
+ def test_str(self):
+ commit = Commit(self.repo, id='abc')
+ assert_equal ("abc", str(commit))
+
+ def test_repr(self):
+ commit = Commit(self.repo, id='abc')
+ assert_equal('<GitPython.Commit "abc">', repr(commit))
diff --git a/test/git/test_diff.py b/test/git/test_diff.py
new file mode 100644
index 00000000..4c1fa7fb
--- /dev/null
+++ b/test/git/test_diff.py
@@ -0,0 +1,13 @@
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestDiff(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+
+ def test_list_from_string_new_mode(self):
+ output = fixture('diff_new_mode')
+ diffs = Diff.list_from_string(self.repo, output)
+ assert_equal(1, len(diffs))
+ assert_equal(10, len(diffs[0].diff.splitlines()))
diff --git a/test/git/test_git.py b/test/git/test_git.py
new file mode 100644
index 00000000..dc6f067e
--- /dev/null
+++ b/test/git/test_git.py
@@ -0,0 +1,49 @@
+import os
+from mock import *
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestGit(object):
+ def setup(self):
+ base = os.path.join(os.path.dirname(__file__), "../.."),
+ self.git = Git(base)
+ self.git_bin_base = "%s --git-dir=%s" % (Git.git_binary, base)
+
+ @patch(Git, 'execute')
+ def test_method_missing_calls_execute(self, git):
+ git.return_value = ''
+ self.git.version()
+ assert_true(git.called)
+ # assert_equal(git.call_args, ((("%s version " % self.git_bin_base),), {}))
+
+ def test_it_transforms_kwargs_into_git_command_arguments(self):
+ assert_equal(["-s"], self.git.transform_kwargs(**{'s': True}))
+ assert_equal(["-s 5"], self.git.transform_kwargs(**{'s': 5}))
+
+ assert_equal(["--max-count"], self.git.transform_kwargs(**{'max_count': True}))
+ assert_equal(["--max-count=5"], self.git.transform_kwargs(**{'max_count': 5}))
+
+ assert_equal(["-s", "-t"], self.git.transform_kwargs(**{'s': True, 't': True}))
+
+ def test_it_executes_git_to_shell_and_returns_result(self):
+ assert_match('^git version [\d\.]*$', self.git.execute("%s version" % Git.git_binary))
+
+ def test_it_transforms_kwargs_shell_escapes_arguments(self):
+ assert_equal(["--foo=\"bazz'er\""], self.git.transform_kwargs(**{'foo': "bazz'er"}))
+ assert_equal(["-x \"bazz'er\""], self.git.transform_kwargs(**{'x': "bazz'er"}))
+
+ @patch(Git, 'execute')
+ def test_it_really_shell_escapes_arguments_to_the_git_shell_1(self, git):
+ self.git.foo(**{'bar': "bazz'er"})
+ assert_true(git.called)
+ assert_equal(git.call_args, ((("%s foo --bar=\"bazz'er\"" % self.git_bin_base),), {}))
+
+ @patch(Git, 'execute')
+ def test_it_really_shell_escapes_arguments_to_the_git_shell_2(self, git):
+ self.git.bar(**{'x': "quu'x"})
+ assert_true(git.called)
+ assert_equal(git.call_args, ((("%s bar -x \"quu'x\"" % self.git_bin_base),), {}))
+
+ def test_it_shell_escapes_the_standalone_argument(self):
+ self.git.foo("bar's", {})
diff --git a/test/git/test_head.py b/test/git/test_head.py
new file mode 100644
index 00000000..38b2a5ab
--- /dev/null
+++ b/test/git/test_head.py
@@ -0,0 +1,19 @@
+from mock import *
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestHead(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+
+ @patch(Git, 'method_missing')
+ def test_repr(self, git):
+ git.return_value = fixture('for_each_ref')
+
+ head = self.repo.heads[0]
+
+ assert_equal('<GitPython.Head "%s">' % head.name, repr(head))
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('for_each_ref', 'refs/heads'), {'sort': 'committerdate', 'format': '%(refname)%00%(objectname)'}))
diff --git a/test/git/test_repo.py b/test/git/test_repo.py
new file mode 100644
index 00000000..d7a2f271
--- /dev/null
+++ b/test/git/test_repo.py
@@ -0,0 +1,309 @@
+import os
+import time
+from mock import *
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestRepo(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+
+ @raises(InvalidGitRepositoryError)
+ def test_new_should_raise_on_invalid_repo_location(self):
+ Repo("/tmp")
+
+ @raises(NoSuchPathError)
+ def test_new_should_raise_on_non_existant_path(self):
+ Repo("/foobar")
+
+ def test_description(self):
+ assert_equal("Unnamed repository; edit this file to name it for gitweb.", self.repo.description)
+
+ def test_heads_should_return_array_of_head_objects(self):
+ for head in self.repo.heads:
+ assert_equal(Head, head.__class__)
+
+ @patch(Git, 'method_missing')
+ def test_heads_should_populate_head_data(self, git):
+ # Git.any_instance.expects(:for_each_ref).returns(fixture('for_each_ref'))
+ git.return_value = fixture('for_each_ref')
+
+ head = self.repo.heads[0]
+ assert_equal('master', head.name)
+ assert_equal('634396b2f541a9f2d58b00be1a07f0c358b999b3', head.commit.id)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('for_each_ref', 'refs/heads'), {'sort': 'committerdate', 'format': '%(refname)%00%(objectname)'}))
+
+ @patch(Git, 'method_missing')
+ def test_commits(self, git):
+ # Git.any_instance.expects(:rev_list).returns(fixture('rev_list'))
+ git.return_value = fixture('rev_list')
+
+ commits = self.repo.commits('master', 10)
+
+ c = commits[0]
+ assert_equal('4c8124ffcf4039d292442eeccabdeca5af5c5017', c.id)
+ assert_equal(["634396b2f541a9f2d58b00be1a07f0c358b999b3"], [p.id for p in c.parents])
+ assert_equal("672eca9b7f9e09c22dcb128c283e8c3c8d7697a4", c.tree.id)
+ assert_equal("Tom Preston-Werner", c.author.name)
+ assert_equal("tom@mojombo.com", c.author.email)
+ assert_equal(time.gmtime(1191999972), c.authored_date)
+ assert_equal("Tom Preston-Werner", c.committer.name)
+ assert_equal("tom@mojombo.com", c.committer.email)
+ assert_equal(time.gmtime(1191999972), c.committed_date)
+ assert_equal("implement Grit#heads", c.message)
+
+ c = commits[1]
+ assert_equal([], c.parents)
+
+ c = commits[2]
+ assert_equal(["6e64c55896aabb9a7d8e9f8f296f426d21a78c2c", "7f874954efb9ba35210445be456c74e037ba6af2"], map(lambda p: p.id, c.parents))
+ assert_equal("Merge branch 'site'", c.message)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('rev_list',), {}))
+
+ @patch(Git, 'method_missing')
+ def test_commit_count(self, git):
+ # Git.any_instance.expects(:rev_list).with({}, 'master').returns(fixture('rev_list_count'))
+ git.return_value = fixture('rev_list_count')
+
+ assert_equal(655, self.repo.commit_count('master'))
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('rev_list', 'master'), {}))
+
+ def test_commit(self):
+ commit = self.repo.commit('634396b2f541a9f2d58b00be1a07f0c358b999b3')
+
+ assert_equal("634396b2f541a9f2d58b00be1a07f0c358b999b3", commit.id)
+
+ @patch(Git, 'method_missing')
+ def test_tree(self, git):
+ # Git.any_instance.expects(:ls_tree).returns(fixture('ls_tree_a'))
+ git.return_value = fixture('ls_tree_a')
+
+ tree = self.repo.tree('master')
+
+ assert_equal(4, len([c for c in tree.contents if isinstance(c, Blob)]))
+ assert_equal(3, len([c for c in tree.contents if isinstance(c, Tree)]))
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('ls_tree', 'master'), {}))
+
+ @patch(Git, 'method_missing')
+ def test_blob(self, git):
+ # Git.any_instance.expects(:cat_file).returns(fixture('cat_file_blob'))
+ git.return_value = fixture('cat_file_blob')
+
+ blob = self.repo.blob("abc")
+ assert_equal("Hello world", blob.data)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('cat_file', 'abc'), {'p': True}))
+
+ @patch(Repo, '__init__')
+ @patch(Git, 'method_missing')
+ def test_init_bare(self, repo, git):
+ # Git.any_instance.expects(:init).returns(true)
+ # Repo.expects(:new).with("/foo/bar.git")
+ git.return_value = True
+
+ Repo.init_bare("/foo/bar.git")
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('init',), {}))
+ assert_true(repo.called)
+ assert_equal(repo.call_args, (('/foo/bar.git',), {}))
+
+ @patch(Repo, '__init__')
+ @patch(Git, 'method_missing')
+ def test_init_bare_with_options(self, repo, git):
+ # Git.any_instance.expects(:init).with(
+ # :template => "/baz/sweet").returns(true)
+ # Repo.expects(:new).with("/foo/bar.git")
+ git.return_value = True
+
+ Repo.init_bare("/foo/bar.git", **{'template': "/baz/sweet"})
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('init',), {'template': '/baz/sweet'}))
+ assert_true(repo.called)
+ assert_equal(repo.call_args, (('/foo/bar.git',), {}))
+
+ @patch(Repo, '__init__')
+ @patch(Git, 'method_missing')
+ def test_fork_bare(self, repo, git):
+ # Git.any_instance.expects(:clone).with(
+ # {:bare => true, :shared => false},
+ # "#{absolute_project_path}/.git",
+ # "/foo/bar.git").returns(nil)
+ # Repo.expects(:new)
+ git.return_value = None
+
+ self.repo.fork_bare("/foo/bar.git")
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('clone', '%s/.git' % absolute_project_path(), '/foo/bar.git'), {'bare': True, 'shared': False}))
+ assert_true(repo.called)
+
+ @patch(Repo, '__init__')
+ @patch(Git, 'method_missing')
+ def test_fork_bare_with_options(self, repo, git):
+ # Git.any_instance.expects(:clone).with(
+ # {:bare => true, :shared => false, :template => '/awesome'},
+ # "#{absolute_project_path}/.git",
+ # "/foo/bar.git").returns(nil)
+ # Repo.expects(:new)
+ git.return_value = None
+
+ self.repo.fork_bare("/foo/bar.git", **{'template': '/awesome'})
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('clone', '%s/.git' % absolute_project_path(), '/foo/bar.git'),
+ {'bare': True, 'shared': False, 'template': '/awesome'}))
+ assert_true(repo.called)
+
+ @patch(Git, 'method_missing')
+ def test_diff(self, git):
+ # Git.any_instance.expects(:diff).with({}, 'master^', 'master', '--')
+ self.repo.diff('master^', 'master')
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', 'master^', 'master', '--'), {}))
+
+ # Git.any_instance.expects(:diff).with({}, 'master^', 'master', '--', 'foo/bar')
+ self.repo.diff('master^', 'master', 'foo/bar')
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', 'master^', 'master', '--', 'foo/bar'), {}))
+
+ # Git.any_instance.expects(:diff).with({}, 'master^', 'master', '--', 'foo/bar', 'foo/baz')
+ self.repo.diff('master^', 'master', 'foo/bar', 'foo/baz')
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('diff', 'master^', 'master', '--', 'foo/bar', 'foo/baz'), {}))
+
+ @patch(Git, 'method_missing')
+ def test_diff(self, git):
+ # Git.any_instance.expects(:diff).returns(fixture('diff_p'))
+ git.return_value = fixture('diff_p')
+
+ diffs = self.repo.commit_diff('master')
+ assert_equal(15, len(diffs))
+ assert_true(git.called)
+
+ def test_archive_tar(self):
+ self.repo.archive_tar
+
+ def test_archive_tar_gz(self):
+ self.repo.archive_tar_gz
+
+ @patch('gitalicious.lib.utils', 'touch')
+ def test_enable_daemon_serve(self, touch):
+ # FileUtils.expects(:touch).with(File.join(self.repo.path, '.git', 'git-daemon-export-ok'))
+ self.repo.enable_daemon_serve
+
+ def test_disable_daemon_serve(self):
+ # FileUtils.expects(:rm_f).with(File.join(self.repo.path, '.git', 'git-daemon-export-ok'))
+ self.repo.disable_daemon_serve
+
+ @patch(os.path, 'exists')
+ @patch('__builtin__', 'open')
+ def test_alternates_with_two_alternates(self, exists, read):
+ # File.expects(:exist?).with("#{absolute_project_path}/.git/objects/info/alternates").returns(true)
+ # File.expects(:read).returns("/path/to/repo1/.git/objects\n/path/to/repo2.git/objects\n")
+ exists.return_value = True
+ read.return_value = ("/path/to/repo1/.git/objects\n/path/to/repo2.git/objects\n")
+
+ assert_equal(["/path/to/repo1/.git/objects", "/path/to/repo2.git/objects"], self.repo.alternates)
+
+ assert_true(exists.called)
+ assert_true(read.called)
+
+ @patch(os.path, 'exists')
+ def test_alternates_no_file(self, os):
+ os.return_value = False
+ # File.expects(:exist?).returns(false)
+ assert_equal([], self.repo.alternates)
+
+ assert_true(os.called)
+
+ @patch(os.path, 'exists')
+ def test_alternates_setter_ok(self, os):
+ os.return_value = True
+ alts = ['/path/to/repo.git/objects', '/path/to/repo2.git/objects']
+
+ # File.any_instance.expects(:write).with(alts.join("\n"))
+
+ self.repo.alternates = alts
+
+ assert_true(os.called)
+ # assert_equal(os.call_args, ((alts,), {}))
+ # for alt in alts:
+
+ @patch(os.path, 'exists')
+ @raises(NoSuchPathError)
+ def test_alternates_setter_bad(self, os):
+ os.return_value = False
+
+ alts = ['/path/to/repo.git/objects']
+ # File.any_instance.expects(:write).never
+ self.repo.alternates = alts
+
+ for alt in alts:
+ assert_true(os.called)
+ assert_equal(os.call_args, (alt, {}))
+
+ @patch(os, 'remove')
+ def test_alternates_setter_empty(self, os):
+ self.repo.alternates = []
+ assert_true(os.called)
+
+ def test_inspect(self):
+ assert_equal('<GitPython.Repo "%s/.git">' % os.path.abspath(GIT_REPO), repr(self.repo))
+
+ @patch(Git, 'method_missing')
+ def test_log(self, git):
+ git.return_value = fixture('rev_list')
+ assert_equal('4c8124ffcf4039d292442eeccabdeca5af5c5017', self.repo.log()[0].id)
+ assert_equal('ab25fd8483882c3bda8a458ad2965d2248654335', self.repo.log()[-1].id)
+ assert_true(git.called)
+ assert_equal(git.call_count, 2)
+ assert_equal(git.call_args, (('log', 'master'), {'pretty': 'raw'}))
+
+ @patch(Git, 'method_missing')
+ def test_log_with_path_and_options(self, git):
+ git.return_value = fixture('rev_list')
+ self.repo.log('master', 'file.rb', **{'max_count': 1})
+ assert_true(git.called)
+ assert_equal(git.call_args, (('log', 'master', '--', 'file.rb'), {'pretty': 'raw', 'max_count': 1}))
+
+ @patch(Git, 'method_missing')
+ @patch(Git, 'method_missing')
+ def test_commit_deltas_from_nothing_new(self, gitb, gita):
+ gitb.return_value = fixture("rev_list_delta_b")
+ gita.return_value = fixture("rev_list_delta_a")
+ other_repo = Repo(GIT_REPO)
+ # self.repo.git.expects(:rev_list).with({}, "master").returns(fixture("rev_list_delta_b"))
+ # other_repo.git.expects(:rev_list).with({}, "master").returns(fixture("rev_list_delta_a"))
+
+ delta_commits = self.repo.commit_deltas_from(other_repo)
+ assert_equal(0, len(delta_commits))
+ assert_true(gitb.called)
+ assert_equal(gitb.call_args, (('rev_list', 'master'), {}))
+ assert_true(gita.called)
+ assert_equal(gita.call_args, (('rev_list', 'master'), {}))
+
+ def test_commit_deltas_from_when_other_has_new(self):
+ other_repo = Repo(GIT_REPO)
+ # self.repo.git.expects(:rev_list).with({}, "master").returns(fixture("rev_list_delta_a"))
+ # other_repo.git.expects(:rev_list).with({}, "master").returns(fixture("rev_list_delta_b"))
+ # for ref in ['4c8124ffcf4039d292442eeccabdeca5af5c5017',
+ # '634396b2f541a9f2d58b00be1a07f0c358b999b3',
+ # 'ab25fd8483882c3bda8a458ad2965d2248654335']:
+ # Commit.expects(:find_all).with(other_repo, ref, :max_count => 1).returns([stub()])
+ delta_commits = self.repo.commit_deltas_from(other_repo)
+ assert_equal(3, len(delta_commits))
diff --git a/test/git/test_stats.py b/test/git/test_stats.py
new file mode 100644
index 00000000..b095b47e
--- /dev/null
+++ b/test/git/test_stats.py
@@ -0,0 +1,22 @@
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestStats(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+
+ def test_list_from_string(self):
+ output = fixture('diff_numstat')
+ stats = Stats.list_from_string(self.repo, output)
+
+ assert_equal(2, stats.total['files'])
+ assert_equal(52, stats.total['lines'])
+ assert_equal(29, stats.total['insertions'])
+ assert_equal(23, stats.total['deletions'])
+
+ assert_equal(29, stats.files["a.txt"]['insertions'])
+ assert_equal(18, stats.files["a.txt"]['deletions'])
+
+ assert_equal(0, stats.files["b.txt"]['insertions'])
+ assert_equal(5, stats.files["b.txt"]['deletions'])
diff --git a/test/git/test_tag.py b/test/git/test_tag.py
new file mode 100644
index 00000000..15b5bbbe
--- /dev/null
+++ b/test/git/test_tag.py
@@ -0,0 +1,31 @@
+from mock import *
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestTag(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+
+ @patch(Git, 'method_missing')
+ def test_list_from_string(self, git):
+ git.return_value = fixture('for_each_ref_tags')
+
+ tags = self.repo.tags
+
+ assert_equal(1, len(tags))
+ assert_equal('v0.7.1', tags[0].name)
+ assert_equal('634396b2f541a9f2d58b00be1a07f0c358b999b3', tags[0].commit.id)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('for_each_ref', 'refs/tags'), {'sort': 'committerdate', 'format': '%(refname)%00%(objectname)'}))
+
+ @patch(Git, 'method_missing')
+ def test_repr(self, git):
+ git.return_value = fixture('for_each_ref')
+
+ tag = self.repo.tags[0]
+ assert_equal('<GitPython.Tag "%s">' % tag.name, repr(tag))
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('for_each_ref', 'refs/tags'), {'sort': 'committerdate', 'format': '%(refname)%00%(objectname)'}))
diff --git a/test/git/test_tree.py b/test/git/test_tree.py
new file mode 100644
index 00000000..3812f213
--- /dev/null
+++ b/test/git/test_tree.py
@@ -0,0 +1,83 @@
+from mock import *
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestTree(object):
+ def setup(self):
+ self.repo = Repo(GIT_REPO)
+ self.tree = Tree(self.repo)
+
+ @patch(Git, 'method_missing')
+ def test_contents_should_cache(self, git):
+ git.return_value = fixture('ls_tree_a') + fixture('ls_tree_b')
+
+ tree = self.repo.tree('master')
+
+ child = tree.contents[-1]
+ child.contents
+ child.contents
+
+ assert_true(git.called)
+ assert_equal(2, git.call_count)
+ assert_equal(git.call_args, (('ls_tree', '34868e6e7384cb5ee51c543a8187fdff2675b5a7'), {}))
+
+ def test_content_from_string_tree_should_return_tree(self):
+ text = fixture('ls_tree_a').splitlines()[-1]
+ tree = self.tree.content_from_string(None, text)
+
+ assert_equal(Tree, tree.__class__)
+ assert_equal("650fa3f0c17f1edb4ae53d8dcca4ac59d86e6c44", tree.id)
+ assert_equal("040000", tree.mode)
+ assert_equal("test", tree.name)
+
+ def test_content_from_string_tree_should_return_blob(self):
+ text = fixture('ls_tree_b').split("\n")[0]
+
+ tree = self.tree.content_from_string(None, text)
+
+ assert_equal(Blob, tree.__class__)
+ assert_equal("aa94e396335d2957ca92606f909e53e7beaf3fbb", tree.id)
+ assert_equal("100644", tree.mode)
+ assert_equal("grit.rb", tree.name)
+
+ def test_content_from_string_tree_should_return_commit(self):
+ text = fixture('ls_tree_commit').split("\n")[1]
+
+ tree = self.tree.content_from_string(None, text)
+ assert_none(tree)
+
+ @raises(TypeError)
+ def test_content_from_string_invalid_type_should_raise(self):
+ self.tree.content_from_string(None, "040000 bogus 650fa3f0c17f1edb4ae53d8dcca4ac59d86e6c44 test")
+
+ @patch(Blob, '__len__')
+ @patch(Git, 'method_missing')
+ def test_slash(self, blob, git):
+ git.return_value = fixture('ls_tree_a')
+ blob.return_value = 1
+
+ tree = self.repo.tree('master')
+
+ assert_equal('aa06ba24b4e3f463b3c4a85469d0fb9e5b421cf8', (tree/'lib').id)
+ assert_equal('8b1e02c0fb554eed2ce2ef737a68bb369d7527df', (tree/'README.txt').id)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('ls_tree', 'master'), {}))
+
+ @patch(Git, 'method_missing')
+ def test_slash_with_commits(self, git):
+ git.return_value = fixture('ls_tree_commit')
+
+ tree = self.repo.tree('master')
+
+ assert_none(tree/'bar')
+ assert_equal('2afb47bcedf21663580d5e6d2f406f08f3f65f19', (tree/'foo').id)
+ assert_equal('f623ee576a09ca491c4a27e48c0dfe04be5f4a2e', (tree/'baz').id)
+
+ assert_true(git.called)
+ assert_equal(git.call_args, (('ls_tree', 'master'), {}))
+
+ def test_repr(self):
+ self.tree = Tree(self.repo, **{'id': 'abc'})
+ assert_equal('<GitPython.Tree "abc">', repr(self.tree))
diff --git a/test/git/test_utils.py b/test/git/test_utils.py
new file mode 100644
index 00000000..2147e4cb
--- /dev/null
+++ b/test/git/test_utils.py
@@ -0,0 +1,17 @@
+import os
+from gitalicious.test.asserts import *
+from gitalicious.lib import *
+from gitalicious.test.helper import *
+
+class TestUtils(object):
+ def setup(self):
+ base = os.path.join(os.path.dirname(__file__), "../.."),
+ self.git = Git(base)
+ self.git_bin_base = "%s --git-dir='%s'" % (Git.git_binary, base)
+
+ def test_it_escapes_single_quotes_with_shell_escape(self):
+ assert_equal("\\\\'foo", shell_escape("'foo"))
+
+ def test_it_should_dashify(self):
+ assert_equal('this-is-my-argument', dashify('this_is_my_argument'))
+ assert_equal('foo', dashify('foo'))
diff --git a/test/helper.py b/test/helper.py
new file mode 100644
index 00000000..082e57e5
--- /dev/null
+++ b/test/helper.py
@@ -0,0 +1,10 @@
+import os
+
+GIT_REPO = os.path.join(os.path.dirname(__file__), "..")
+
+def fixture(name):
+ file = open(os.path.join(os.path.dirname(__file__), "fixtures", name))
+ return file.read()
+
+def absolute_project_path():
+ return os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) \ No newline at end of file