diff options
author | Sebastian Thiel <byronimo@gmail.com> | 2009-10-15 10:04:17 +0200 |
---|---|---|
committer | Sebastian Thiel <byronimo@gmail.com> | 2009-10-15 10:04:17 +0200 |
commit | 6226720b0e6a5f7cb9223fc50363def487831315 (patch) | |
tree | 10f70f8e41c91f5bf57f04b616f3e5afdb9f8407 | |
parent | b0e84a3401c84507dc017d6e4f57a9dfdb31de53 (diff) | |
parent | 4186a2dbbd48fd67ff88075c63bbd3e6c1d8a2df (diff) | |
download | gitpython-6226720b0e6a5f7cb9223fc50363def487831315.tar.gz |
Initial set of improvementes merged into master, including a class hierarchy redesign and performance improvements
Merge commit 'origin/improvements'
* commit 'origin/improvements': (38 commits)
test_performance: module containing benchmarks to get an idea of the achieved throughput
Removed plenty of mocked tree tests as they cannot work anymore with persistent commands that require stdin AND binary data - not even an adapter would help here. These tests will have to be replaced.
tree: now reads tress directly by parsing the binary data, allowing it to safe possibly hundreds of command calls
Refs are now truly dynamic - this costs a little bit of (persistent command) work, but assures refs behave as expected
persistent command signature changed to also return the hexsha from a possible input ref - the objects pointed to by refs are now baked on demand - perhaps it should change to always be re-retrieved using a property as it is relatively fast - this way refs can always be cached
test_blob: removed many redundant tests that would fail now as the mock cannot handle the complexity of the command backend
Implemented git command facility to keep persistent commands for fast object information retrieval
test: Added time-consuming test which could also be a benchmark in fact - currently it cause hundreds of command invocations which is slow
cmd: added option to return the process directly, allowing to read the output directly from the output stream
added Iterable interface to Ref type
renamed find_all to list_all, changed commit to use iterable interface in preparation for command changes
Added base for all iteratable objects
unified name of utils module, recently it was named util and utils in different packages
tree: renamed content_from_string to _from_string to make it private. Removed tests that were testing that method
tree: now behaves like a list with string indexing functionality - using a dict as cache is a problem as the tree is ordered, added blobs, trees and traverse method
test_base: Improved basic object creation as well as set hash tests
repo.active_branch now returns a Head object, not a string
IndexObjects are now checking their slots to raise a proper error message in case someone tries to access an unset path or mode - this information cannot be retrieved afterwards as IndexObject information is kept in the object that pointed at them. To find this information, one would have to search all objects which is not feasible
refs now take repo as first argument and derive from LazyMixin to allow them to dynamically retrieve their objects
renamed from_string and list_from_string to _from_string and _list_from_string to indicate their new status as private method, adjusted all callers respectively
...
45 files changed, 3325 insertions, 2746 deletions
@@ -1,6 +1,75 @@ ======= CHANGES ======= + +0.2 +===== +General +------- +* file mode in Tree, Blob and Diff objects now is an int compatible to definintiions + in the stat module, allowing you to query whether individual user, group and other + read, write and execute bits are set. +* Adjusted class hierarchy to generally allow comparison and hash for Objects and Refs +* Improved Tag object which now is a Ref that may contain a tag object with additional + Information +* id_abbrev method has been removed as it could not assure the returned short SHA's + where unique +* removed basename method from Objects with path's as it replicated features of os.path +* from_string and list_from_string methods are now private and were renamed to + _from_string and _list_from_string respectively. As part of the private API, they + may change without prior notice. +* Renamed all find_all methods to list_items - this method is part of the Iterable interface + that also provides a more efficients and more responsive iter_items method + +Item Iteration +-------------- +* Previously one would return and process multiple items as list only which can + hurt performance and memory consumption and reduce response times. + iter_items method provide an iterator that will return items on demand as parsed + from a stream. This way any amount of objects can be handled. + +objects Package +---------------- +* blob, tree, tag and commit module have been moved to new objects package. This should + not affect you though unless you explicitly imported individual objects. If you just + used the git package, names did not change. + +Repo +---- +* Moved blame method from Blob to repo as it appeared to belong there much more. +* active_branch method now returns a Head object instead of a string with the name + of the active branch. +* tree method now requires a Ref instance as input and defaults to the active_branche + instead of master +* Removed 'log' method as it as effectively the same as the 'commits' method +* 'commits' method has no max-count of returned commits anymore, it now behaves + like git-rev-list + +Diff +---- +* Members a a_commit and b_commit renamed to a_blob and b_blob - they are populated + with Blob objects if possible +* Members a_path and b_path removed as this information is kept in the blobs + +Blob +---- +* former 'name' member renamed to path as it suits the actual data better + +Tree +---- +* former 'name' member renamed to path as it suits the actual data better +* added traverse method allowing to recursively traverse tree items +* deleted blob method +* added blobs and trees properties allowing to query the respective items in the + tree +* now mimics behaviour of a read-only list instead of a dict to maintain order. +* content_from_string method is now private and not part of the public API anymore + +Refs +---- +* Will dynmically retrieve their object at the time of query to assure the information + is actual. Recently objects would be cached, hence ref object not be safely kept + persistent. 0.1.6 ===== @@ -49,20 +118,6 @@ Repo * Corrected ``commits_between`` always returning None instead of the reversed list. - -0.1.X -===== -( Future Release ) -General -------- -* See changes in Diff class as your client code needs adjustments to work with it - -Diff ----- -* Members a a_commit and b_commit renamed to a_blob and b_blob - they are populated - with Blob objects if possible -* Members a_path and b_path removed as this information is kept in the blobs - 0.1.5 ===== diff --git a/ez_setup.py b/ez_setup.py index 3031ad0d..50d0075b 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -4,8 +4,8 @@ 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() + 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 @@ -15,106 +15,106 @@ 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] +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', + '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 + 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 + 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) + """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 + 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(""" + """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 +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. @@ -125,96 +125,96 @@ I will start the download in %d seconds. 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) + 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.)' + """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""" + """Update our built-in md5 registry""" - import re - from md5 import md5 + 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() + 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) + 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() + 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) + 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() + 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:]) + 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/__init__.py b/lib/git/__init__.py index 28d14d0c..6f482128 100644 --- a/lib/git/__init__.py +++ b/lib/git/__init__.py @@ -9,19 +9,17 @@ import inspect __version__ = 'git' +from git.objects import * +from git.refs import * from git.actor import Actor -from git.blob import Blob -from git.commit import Commit from git.diff import Diff from git.errors import InvalidGitRepositoryError, NoSuchPathError, GitCommandError from git.cmd import Git -from git.head import Head from git.repo import Repo from git.stats import Stats -from git.tag import Tag -from git.tree import Tree from git.utils import dashify from git.utils import touch + __all__ = [ name for name, obj in locals().items() - if not (name.startswith('_') or inspect.ismodule(obj)) ] + if not (name.startswith('_') or inspect.ismodule(obj)) ] diff --git a/lib/git/actor.py b/lib/git/actor.py index bc1a4479..fe4a47e5 100644 --- a/lib/git/actor.py +++ b/lib/git/actor.py @@ -7,36 +7,40 @@ import re class Actor(object): - """Actors hold information about a person acting on the repository. They - can be committers and authors or anything with a name and an email as - mentioned in the git log entries.""" - def __init__(self, name, email): - self.name = name - self.email = email - - def __str__(self): - return self.name - - def __repr__(self): - return '<git.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) + """Actors hold information about a person acting on the repository. They + can be committers and authors or anything with a name and an email as + mentioned in the git log entries.""" + # precompiled regex + name_only_regex = re.compile( r'<.+>' ) + name_email_regex = re.compile( r'(.*) <(.+?)>' ) + + def __init__(self, name, email): + self.name = name + self.email = email + + def __str__(self): + return self.name + + def __repr__(self): + return '<git.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 cls.name_only_regex.search(string): + m = cls.name_email_regex.search(string) + name, email = m.groups() + return Actor(name, email) + else: + return Actor(string, None) diff --git a/lib/git/blob.py b/lib/git/blob.py deleted file mode 100644 index 82a41f73..00000000 --- a/lib/git/blob.py +++ /dev/null @@ -1,161 +0,0 @@ -# blob.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -import mimetypes -import os -import re -import time -from actor import Actor -from commit import Commit - -class Blob(object): - """A Blob encapsulates a git blob object""" - DEFAULT_MIME_TYPE = "text/plain" - - def __init__(self, repo, id, mode=None, name=None): - """ - Create an unbaked Blob containing just the specified attributes - - ``repo`` - is the Repo - - ``id`` - is the git object id - - ``mode`` - is the file mode - - ``name`` - is the file name - - Returns - git.Blob - """ - self.repo = repo - self.id = id - self.mode = mode - self.name = name - - self._size = None - self.data_stored = None - - @property - def size(self): - """ - The size of this blob in bytes - - Returns - int - - NOTE - The size will be cached after the first access - """ - if self._size is None: - self._size = 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 - - NOTE - The data will be cached after the first access. - """ - self.data_stored = self.data_stored or self.repo.git.cat_file(self.id, p=True, with_raw_output=True) - return self.data_stored - - @property - def mime_type(self): - """ - The mime type of this file (based on the filename) - - Returns - str - - NOTE - Defaults to 'text/plain' in case the actual file type is unknown. - """ - 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): - """ - Returns - The basename of the Blobs file name - """ - 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: [git.Commit, list: [<line>]] - A list of tuples associating a Commit object with a list of lines that - changed within the given commit. The Commit objects will be given in order - of appearance. - """ - 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].append( text ) - info = None - - return blames - - def __repr__(self): - return '<git.Blob "%s">' % self.id diff --git a/lib/git/cmd.py b/lib/git/cmd.py index aef53350..2965eb8b 100644 --- a/lib/git/cmd.py +++ b/lib/git/cmd.py @@ -6,7 +6,6 @@ import os, sys import subprocess -import re from utils import * from errors import GitCommandError @@ -14,208 +13,326 @@ from errors import GitCommandError GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output', - 'with_exceptions', 'with_raw_output') + 'with_exceptions', 'with_raw_output', 'as_process') extra = {} if sys.platform == 'win32': - extra = {'shell': True} + extra = {'shell': True} class Git(object): - """ - The Git class manages communication with the Git binary. - + """ + The Git class manages communication with the Git binary. + It provides a convenient interface to calling the Git binary, such as in:: g = Git( git_dir ) - g.init() # calls 'git init' program + g.init() # calls 'git init' program rval = g.ls_files() # calls 'git ls-files' program ``Debugging`` - Set the GIT_PYTHON_TRACE environment variable print each invocation - of the command to stdout. - Set its value to 'full' to see details about the returned values. - """ - def __init__(self, git_dir=None): - """ - Initialize this instance with: - - ``git_dir`` - Git directory we should work in. If None, we always work in the current - directory as returned by os.getcwd() - """ - super(Git, self).__init__() - self.git_dir = git_dir - - def __getattr__(self, name): - """ - A convenience method as it allows to call the command as if it was - an object. - Returns - Callable object that will execute call _call_process with your arguments. - """ - if name[:1] == '_': - raise AttributeError(name) - return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) - - @property - def get_dir(self): - """ - Returns - Git directory we are working on - """ - return self.git_dir - - def execute(self, command, - istream=None, - with_keep_cwd=False, - with_extended_output=False, - with_exceptions=True, - with_raw_output=False, - ): - """ - Handles executing the command on the shell and consumes and returns - the returned information (stdout) - - ``command`` - The command argument list to execute. - It should be a string, or a sequence of program arguments. The - program to execute is the first item in the args sequence or string. - - ``istream`` - Standard input filehandle passed to subprocess.Popen. - - ``with_keep_cwd`` - Whether to use the current working directory from os.getcwd(). - GitPython uses get_work_tree() as its working directory by - default and get_git_dir() for bare repositories. - - ``with_extended_output`` - Whether to return a (status, stdout, stderr) tuple. - - ``with_exceptions`` - Whether to raise an exception when git returns a non-zero status. - - ``with_raw_output`` - Whether to avoid stripping off trailing whitespace. - - Returns:: - - str(output) # extended_output = False (Default) - tuple(int(status), str(stdout), str(stderr)) # extended_output = True - - Raise - GitCommandError - - NOTE - If you add additional keyword arguments to the signature of this method, - you must update the execute_kwargs tuple housed in this module. - """ - if GIT_PYTHON_TRACE and not GIT_PYTHON_TRACE == 'full': - print ' '.join(command) - - # Allow the user to have the command executed in their working dir. - if with_keep_cwd or self.git_dir is None: - cwd = os.getcwd() - else: - cwd=self.git_dir - - # Start the process - proc = subprocess.Popen(command, - cwd=cwd, - stdin=istream, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - **extra - ) - - # Wait for the process to return - try: - stdout_value = proc.stdout.read() - stderr_value = proc.stderr.read() - status = proc.wait() - finally: - proc.stdout.close() - proc.stderr.close() - - # Strip off trailing whitespace by default - if not with_raw_output: - stdout_value = stdout_value.rstrip() - stderr_value = stderr_value.rstrip() - - if with_exceptions and status != 0: - raise GitCommandError(command, status, stderr_value) - - if GIT_PYTHON_TRACE == 'full': - if stderr_value: - print "%s -> %d: '%s' !! '%s'" % (command, status, stdout_value, stderr_value) - elif stdout_value: - print "%s -> %d: '%s'" % (command, status, stdout_value) - else: - print "%s -> %d" % (command, status) - - # Allow access to the command's status code - if with_extended_output: - return (status, stdout_value, stderr_value) - else: - 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) - elif type(v) is not bool: - args.append("-%s%s" % (k, v)) - else: - if v is True: - args.append("--%s" % dashify(k)) - elif type(v) is not bool: - args.append("--%s=%s" % (dashify(k), v)) - return args - - def _call_process(self, method, *args, **kwargs): - """ - Run the given git command with the specified arguments and return - the result as a String - - ``method`` - is the command. Contained "_" characters will be converted to dashes, - such as in 'ls_files' to call 'ls-files'. - - ``args`` - is the list of arguments - - ``kwargs`` - is a dict of keyword arguments. - This function accepts the same optional keyword arguments - as execute(). - - Examples:: - git.rev_list('master', max_count=10, header=True) - - Returns - Same as execute() - """ - - # Handle optional arguments prior to calling transform_kwargs - # otherwise these'll end up in args, which is bad. - _kwargs = {} - for kwarg in execute_kwargs: - try: - _kwargs[kwarg] = kwargs.pop(kwarg) - except KeyError: - pass - - # Prepare the argument list - opt_args = self.transform_kwargs(**kwargs) - ext_args = map(str, args) - args = opt_args + ext_args - - call = ["git", dashify(method)] - call.extend(args) - - return self.execute(call, **_kwargs) + Set the GIT_PYTHON_TRACE environment variable print each invocation + of the command to stdout. + Set its value to 'full' to see details about the returned values. + """ + class AutoInterrupt(object): + """ + Kill/Interrupt the stored process instance once this instance goes out of scope. It is + used to prevent processes piling up in case iterators stop reading. + Besides all attributes are wired through to the contained process object + """ + __slots__= "proc" + + def __init__(self, proc ): + self.proc = proc + + def __del__(self): + # did the process finish already so we have a return code ? + if self.proc.poll() is not None: + return + + # try to kill it + try: + os.kill(self.proc.pid, 2) # interrupt signal + except AttributeError: + # try windows + subprocess.call(("TASKKILL", "/T", "/PID", self.proc.pid)) + # END exception handling + + def __getattr__(self, attr): + return getattr(self.proc, attr) + + + def __init__(self, git_dir=None): + """ + Initialize this instance with: + + ``git_dir`` + Git directory we should work in. If None, we always work in the current + directory as returned by os.getcwd() + """ + super(Git, self).__init__() + self.git_dir = git_dir + + # cached command slots + self.cat_file_header = None + self.cat_file_all = None + + def __getattr__(self, name): + """ + A convenience method as it allows to call the command as if it was + an object. + Returns + Callable object that will execute call _call_process with your arguments. + """ + if name[:1] == '_': + raise AttributeError(name) + return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) + + @property + def get_dir(self): + """ + Returns + Git directory we are working on + """ + return self.git_dir + + def execute(self, command, + istream=None, + with_keep_cwd=False, + with_extended_output=False, + with_exceptions=True, + with_raw_output=False, + as_process=False + ): + """ + Handles executing the command on the shell and consumes and returns + the returned information (stdout) + + ``command`` + The command argument list to execute. + It should be a string, or a sequence of program arguments. The + program to execute is the first item in the args sequence or string. + + ``istream`` + Standard input filehandle passed to subprocess.Popen. + + ``with_keep_cwd`` + Whether to use the current working directory from os.getcwd(). + GitPython uses get_work_tree() as its working directory by + default and get_git_dir() for bare repositories. + + ``with_extended_output`` + Whether to return a (status, stdout, stderr) tuple. + + ``with_exceptions`` + Whether to raise an exception when git returns a non-zero status. + + ``with_raw_output`` + Whether to avoid stripping off trailing whitespace. + + ``as_process`` + Whether to return the created process instance directly from which + streams can be read on demand. This will render with_extended_output, + with_exceptions and with_raw_output ineffective - the caller will have + to deal with the details himself. + It is important to note that the process will be placed into an AutoInterrupt + wrapper that will interrupt the process once it goes out of scope. If you + use the command in iterators, you should pass the whole process instance + instead of a single stream. + + Returns:: + + str(output) # extended_output = False (Default) + tuple(int(status), str(stdout), str(stderr)) # extended_output = True + + Raise + GitCommandError + + NOTE + If you add additional keyword arguments to the signature of this method, + you must update the execute_kwargs tuple housed in this module. + """ + if GIT_PYTHON_TRACE and not GIT_PYTHON_TRACE == 'full': + print ' '.join(command) + + # Allow the user to have the command executed in their working dir. + if with_keep_cwd or self.git_dir is None: + cwd = os.getcwd() + else: + cwd=self.git_dir + + # Start the process + proc = subprocess.Popen(command, + cwd=cwd, + stdin=istream, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + **extra + ) + + if as_process: + return self.AutoInterrupt(proc) + + # Wait for the process to return + status = 0 + try: + stdout_value = proc.stdout.read() + stderr_value = proc.stderr.read() + status = proc.wait() + finally: + proc.stdout.close() + proc.stderr.close() + + # Strip off trailing whitespace by default + if not with_raw_output: + stdout_value = stdout_value.rstrip() + stderr_value = stderr_value.rstrip() + + if with_exceptions and status != 0: + raise GitCommandError(command, status, stderr_value) + + if GIT_PYTHON_TRACE == 'full': + if stderr_value: + print "%s -> %d: '%s' !! '%s'" % (command, status, stdout_value, stderr_value) + elif stdout_value: + print "%s -> %d: '%s'" % (command, status, stdout_value) + else: + print "%s -> %d" % (command, status) + + # Allow access to the command's status code + if with_extended_output: + return (status, stdout_value, stderr_value) + else: + 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) + elif type(v) is not bool: + args.append("-%s%s" % (k, v)) + else: + if v is True: + args.append("--%s" % dashify(k)) + elif type(v) is not bool: + args.append("--%s=%s" % (dashify(k), v)) + return args + + def _call_process(self, method, *args, **kwargs): + """ + Run the given git command with the specified arguments and return + the result as a String + + ``method`` + is the command. Contained "_" characters will be converted to dashes, + such as in 'ls_files' to call 'ls-files'. + + ``args`` + is the list of arguments + + ``kwargs`` + is a dict of keyword arguments. + This function accepts the same optional keyword arguments + as execute(). + + Examples:: + git.rev_list('master', max_count=10, header=True) + + Returns + Same as execute() + """ + + # Handle optional arguments prior to calling transform_kwargs + # otherwise these'll end up in args, which is bad. + _kwargs = {} + for kwarg in execute_kwargs: + try: + _kwargs[kwarg] = kwargs.pop(kwarg) + except KeyError: + pass + + # Prepare the argument list + opt_args = self.transform_kwargs(**kwargs) + ext_args = map(str, args) + args = opt_args + ext_args + + call = ["git", dashify(method)] + call.extend(args) + + return self.execute(call, **_kwargs) + + def _parse_object_header(self, header_line): + """ + ``header_line`` + <hex_sha> type_string size_as_int + + Returns + (hex_sha, type_string, size_as_int) + + Raises + ValueError if the header contains indication for an error due to incorrect + input sha + """ + tokens = header_line.split() + if len(tokens) != 3: + raise ValueError( "SHA named %s could not be resolved" % tokens[0] ) + + return (tokens[0], tokens[1], int(tokens[2])) + + def __prepare_ref(self, ref): + # required for command to separate refs on stdin + refstr = str(ref) # could be ref-object + if refstr.endswith("\n"): + return refstr + return refstr + "\n" + + def __get_persistent_cmd(self, attr_name, cmd_name, *args,**kwargs): + cur_val = getattr(self, attr_name) + if cur_val is not None: + return cur_val + + options = { "istream" : subprocess.PIPE, "as_process" : True } + options.update( kwargs ) + + cmd = self._call_process( cmd_name, *args, **options ) + setattr(self, attr_name, cmd ) + return cmd + + def __get_object_header(self, cmd, ref): + cmd.stdin.write(self.__prepare_ref(ref)) + cmd.stdin.flush() + return self._parse_object_header(cmd.stdout.readline()) + + def get_object_header(self, ref): + """ + Use this method to quickly examine the type and size of the object behind + the given ref. + + NOTE + The method will only suffer from the costs of command invocation + once and reuses the command in subsequent calls. + + Return: + (hexsha, type_string, size_as_int) + """ + cmd = self.__get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) + return self.__get_object_header(cmd, ref) + + def get_object_data(self, ref): + """ + As get_object_header, but returns object data as well + + Return: + (hexsha, type_string, size_as_int,data_string) + """ + cmd = self.__get_persistent_cmd("cat_file_all", "cat_file", batch=True) + hexsha, typename, size = self.__get_object_header(cmd, ref) + data = cmd.stdout.read(size) + cmd.stdout.read(1) # finishing newlines + + return (hexsha, typename, size, data) diff --git a/lib/git/commit.py b/lib/git/commit.py deleted file mode 100644 index edfe47ca..00000000 --- a/lib/git/commit.py +++ /dev/null @@ -1,296 +0,0 @@ -# commit.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -import re -import time - -from actor import Actor -from lazy import LazyMixin -from tree import Tree -import diff -import stats - -class Commit(LazyMixin): - """ - Wraps a git Commit object. - - This class will act lazily on some of its attributes and will query the - value on demand only if it involves calling the git binary. - """ - def __init__(self, repo, id, tree=None, author=None, authored_date=None, - committer=None, committed_date=None, message=None, parents=None): - """ - Instantiate a new Commit. All keyword arguments taking None as default will - be implicitly set if id names a valid sha. - - The parameter documentation indicates the type of the argument after a colon ':'. - - ``id`` - is the sha id of the commit - - ``parents`` : list( Commit, ... ) - is a list of commit ids - - ``tree`` : Tree - is the corresponding tree id - - ``author`` : Actor - is the author string ( will be implicitly converted into an Actor object ) - - ``authored_date`` : (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst ) - is the authored DateTime - - ``committer`` : Actor - is the committer string - - ``committed_date`` : (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst) - is the committed DateTime - - ``message`` : string - is the commit message - - Returns - git.Commit - """ - LazyMixin.__init__(self) - - self.repo = repo - self.id = id - self.parents = None - self.tree = None - self.author = author - self.authored_date = authored_date - self.committer = committer - self.committed_date = committed_date - self.message = message - - if self.id: - if parents is not None: - self.parents = [Commit(repo, p) for p in parents] - if tree is not None: - self.tree = Tree(repo, id=tree) - - def __eq__(self, other): - return self.id == other.id - - def __ne__(self, other): - return self.id != other.id - - def __bake__(self): - """ - Called by LazyMixin superclass when the first uninitialized member needs - to be set as it is queried. - """ - 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): - """ - Returns - First 7 bytes of the commit's sha id as an abbreviation of the full string. - """ - return self.id[0:7] - - @property - def summary(self): - """ - Returns - First line of the commit message. - """ - return self.message.split('\n', 1)[0] - - @classmethod - def count(cls, repo, ref, path=''): - """ - Count the number of commits reachable from this ref - - ``repo`` - is the Repo - - ``ref`` - is the ref from which to begin (SHA1 or name) - - ``path`` - is an optinal path - - Returns - int - """ - return len(repo.git.rev_list(ref, '--', path).strip().splitlines()) - - @classmethod - def find_all(cls, repo, ref, path='', **kwargs): - """ - Find all commits matching the given criteria. - - ``repo`` - is the Repo - - ``ref`` - is the ref from which to begin (SHA1 or name) - - ``path`` - is an optinal path, if set only Commits that include the path - will be considered - - ``kwargs`` - optional keyword arguments to git where - ``max_count`` is the maximum number of commits to fetch - ``skip`` is the number of commits to skip - - Returns - git.Commit[] - """ - options = {'pretty': 'raw'} - options.update(kwargs) - - output = repo.git.rev_list(ref, '--', path, **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-rev-list command (raw format) - - Returns - git.Commit[] - """ - lines = [l for l in text.splitlines() if l.strip('\r\n')] - - commits = [] - - while lines: - id = lines.pop(0).split()[1] - tree = lines.pop(0).split()[1] - - parents = [] - while lines and lines[0].startswith('parent'): - 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 lines[0].startswith(' '): - messages.append(lines.pop(0).strip()) - - message = '\n'.join(messages) - - 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=None): - """ - Creates diffs between a tree and the index or 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 to. - - Returns - git.Diff[]:: - - between tree and the index if only a is given - between two trees if a and b are given and are commits - """ - paths = paths or [] - - 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('-M', full_index=True, *paths) - return diff.Diff.list_from_string(repo, text) - - @property - def diffs(self): - """ - Returns - git.Diff[] - Diffs between this commit and its first parent or all changes if this - commit is the first commit and has no parent. - """ - if not self.parents: - d = self.repo.git.show(self.id, '-M', 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): - """ - Create a git stat from changes between this commit and its first parent - or from all changes done if this is the very first commit. - - Return - git.Stats - """ - if not self.parents: - text = self.repo.git.diff_tree(self.id, '--', numstat=True, root=True) - text2 = "" - for line in text.splitlines()[1:]: - (insertions, deletions, filename) = line.split("\t") - text2 += "%s\t%s\t%s\n" % (insertions, deletions, 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 '<git.Commit "%s">' % self.id - - @classmethod - def actor(cls, line): - """ - Parse out the actor (author or committer) info - - Returns - [Actor, gmtime(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/diff.py b/lib/git/diff.py index 44f55602..0db83b4f 100644 --- a/lib/git/diff.py +++ b/lib/git/diff.py @@ -5,94 +5,101 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php import re -import blob +import objects.blob as blob class Diff(object): - """ - A Diff contains diff information between two commits. - - It contains two sides a and b of the diff, members are prefixed with - "a" and "b" respectively to inidcate that. - - Diffs keep information about the changed blob objects, the file mode, renames, - deletions and new files. - - There are a few cases where None has to be expected as member variable value: - - ``New File``:: - - a_mode is None - a_blob is None - - ``Deleted File``:: - - b_mode is None - b_blob is NOne - """ + """ + A Diff contains diff information between two commits. + + It contains two sides a and b of the diff, members are prefixed with + "a" and "b" respectively to inidcate that. + + Diffs keep information about the changed blob objects, the file mode, renames, + deletions and new files. + + There are a few cases where None has to be expected as member variable value: + + ``New File``:: + + a_mode is None + a_blob is None + + ``Deleted File``:: + + b_mode is None + b_blob is NOne + """ + + # precompiled regex + re_header = re.compile(r""" + #^diff[ ]--git + [ ]a/(?P<a_path>\S+)[ ]b/(?P<b_path>\S+)\n + (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n + ^rename[ ]from[ ](?P<rename_from>\S+)\n + ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))? + (?:^old[ ]mode[ ](?P<old_mode>\d+)\n + ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? + (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? + (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? + (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) + \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? + """, re.VERBOSE | re.MULTILINE) + re_is_null_hexsha = re.compile( r'^0{40}$' ) + __slots__ = ("a_blob", "b_blob", "a_mode", "b_mode", "new_file", "deleted_file", + "rename_from", "rename_to", "renamed", "diff") - def __init__(self, repo, a_path, b_path, a_blob, b_blob, a_mode, - b_mode, new_file, deleted_file, rename_from, - rename_to, diff): - self.repo = repo + def __init__(self, repo, a_path, b_path, a_blob_id, b_blob_id, a_mode, + b_mode, new_file, deleted_file, rename_from, + rename_to, diff): + if not a_blob_id or self.re_is_null_hexsha.search(a_blob_id): + self.a_blob = None + else: + self.a_blob = blob.Blob(repo, id=a_blob_id, mode=a_mode, path=a_path) + if not b_blob_id or self.re_is_null_hexsha.search(b_blob_id): + self.b_blob = None + else: + self.b_blob = blob.Blob(repo, id=b_blob_id, mode=b_mode, path=b_path) - if not a_blob or re.search(r'^0{40}$', a_blob): - self.a_blob = None - else: - self.a_blob = blob.Blob(repo, id=a_blob, mode=a_mode, name=a_path) - if not b_blob or re.search(r'^0{40}$', b_blob): - self.b_blob = None - else: - self.b_blob = blob.Blob(repo, id=b_blob, mode=b_mode, name=b_path) + self.a_mode = a_mode + self.b_mode = b_mode + if self.a_mode: + self.a_mode = blob.Blob._mode_str_to_int( self.a_mode ) + if self.b_mode: + self.b_mode = blob.Blob._mode_str_to_int( self.b_mode ) + self.new_file = new_file + self.deleted_file = deleted_file + self.rename_from = rename_from + self.rename_to = rename_to + self.renamed = rename_from != rename_to + self.diff = diff - self.a_mode = a_mode - self.b_mode = b_mode - self.new_file = new_file - self.deleted_file = deleted_file - self.rename_from = rename_from - self.rename_to = rename_to - self.renamed = rename_from != rename_to - self.diff = diff + @classmethod + def _list_from_string(cls, repo, text): + """ + Create a new diff object from the given text + ``repo`` + is the repository we are operating on - it is required + + ``text`` + result of 'git diff' between two commits or one commit and the index + + Returns + git.Diff[] + """ + diffs = [] - @classmethod - def list_from_string(cls, repo, text): - """ - Create a new diff object from the given text - ``repo`` - is the repository we are operating on - it is required - - ``text`` - result of 'git diff' between two commits or one commit and the index - - Returns - git.Diff[] - """ - diffs = [] + diff_header = cls.re_header.match + for diff in ('\n' + text).split('\ndiff --git')[1:]: + header = diff_header(diff) - diff_header = re.compile(r""" - #^diff[ ]--git - [ ]a/(?P<a_path>\S+)[ ]b/(?P<b_path>\S+)\n - (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n - ^rename[ ]from[ ](?P<rename_from>\S+)\n - ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))? - (?:^old[ ]mode[ ](?P<old_mode>\d+)\n - ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? - (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? - (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? - (?:^index[ ](?P<a_blob>[0-9A-Fa-f]+) - \.\.(?P<b_blob>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? - """, re.VERBOSE | re.MULTILINE).match + a_path, b_path, similarity_index, rename_from, rename_to, \ + old_mode, new_mode, new_file_mode, deleted_file_mode, \ + a_blob_id, b_blob_id, b_mode = header.groups() + new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode) - for diff in ('\n' + text).split('\ndiff --git')[1:]: - header = diff_header(diff) + diffs.append(Diff(repo, a_path, b_path, a_blob_id, b_blob_id, + old_mode or deleted_file_mode, new_mode or new_file_mode or b_mode, + new_file, deleted_file, rename_from, rename_to, diff[header.end():])) - a_path, b_path, similarity_index, rename_from, rename_to, \ - old_mode, new_mode, new_file_mode, deleted_file_mode, \ - a_blob, b_blob, b_mode = header.groups() - new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode) - - diffs.append(Diff(repo, a_path, b_path, a_blob, b_blob, - old_mode or deleted_file_mode, new_mode or new_file_mode or b_mode, - new_file, deleted_file, rename_from, rename_to, diff[header.end():])) - - return diffs + return diffs diff --git a/lib/git/errors.py b/lib/git/errors.py index 2632d5f3..e9a637c0 100644 --- a/lib/git/errors.py +++ b/lib/git/errors.py @@ -8,25 +8,25 @@ Module containing all exceptions thrown througout the git package, """ class InvalidGitRepositoryError(Exception): - """ - Thrown if the given repository appears to have an invalid format. - """ + """ + Thrown if the given repository appears to have an invalid format. + """ -class NoSuchPathError(Exception): - """ - Thrown if a path could not be access by the system. - """ +class NoSuchPathError(OSError): + """ + Thrown if a path could not be access by the system. + """ class GitCommandError(Exception): - """ - Thrown if execution of the git command fails with non-zero status code. - """ - def __init__(self, command, status, stderr=None): - self.stderr = stderr - self.status = status - self.command = command + """ + Thrown if execution of the git command fails with non-zero status code. + """ + def __init__(self, command, status, stderr=None): + self.stderr = stderr + self.status = status + self.command = command - def __str__(self): - return repr("%s returned exit status %d" % - (str(self.command), self.status)) + def __str__(self): + return repr("%s returned exit status %d" % + (str(self.command), self.status)) diff --git a/lib/git/head.py b/lib/git/head.py deleted file mode 100644 index 639cee40..00000000 --- a/lib/git/head.py +++ /dev/null @@ -1,118 +0,0 @@ -# head.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -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 - <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> - - >>> head.commit.id - '1c09f116cbc2cb4100fb6935bb162daa4723f455' - """ - - def __init__(self, name, commit): - """ - Initialize a newly instanced Head - - `name` - is the name of the head - - `commit` - is the Commit object that the head points to - """ - self.name = name - self.commit = commit - - @classmethod - def find_all(cls, repo, **kwargs): - """ - Find all Heads in the repository - - `repo` - is the Repo - - `kwargs` - Additional options given as keyword arguments, will be passed - to git-for-each-ref - - Returns - git.Head[] - - List is sorted by committerdate - """ - - 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 a list of head objects - - ``repo`` - is the Repo - ``text`` - is the text output from the git-for-each-ref command - - Returns - git.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 - git.Head - """ - full_name, ids = line.split("\x00") - - if full_name.startswith('refs/heads/'): - name = full_name[len('refs/heads/'):] - else: - name = full_name - - c = commit.Commit(repo, id=ids) - return Head(name, c) - - def __repr__(self): - return '<git.Head "%s">' % self.name diff --git a/lib/git/lazy.py b/lib/git/lazy.py deleted file mode 100644 index 5e470181..00000000 --- a/lib/git/lazy.py +++ /dev/null @@ -1,32 +0,0 @@ -# lazy.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -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/objects/__init__.py b/lib/git/objects/__init__.py new file mode 100644 index 00000000..39e650b7 --- /dev/null +++ b/lib/git/objects/__init__.py @@ -0,0 +1,11 @@ +""" +Import all submodules main classes into the package space +""" +import inspect +from tag import * +from blob import * +from tree import * +from commit import * + +__all__ = [ name for name, obj in locals().items() + if not (name.startswith('_') or inspect.ismodule(obj)) ]
\ No newline at end of file diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py new file mode 100644 index 00000000..07538ada --- /dev/null +++ b/lib/git/objects/base.py @@ -0,0 +1,151 @@ +# base.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +import os +from git.utils import LazyMixin + +_assertion_msg_format = "Created object %r whose python type %r disagrees with the acutal git object type %r" + +class Object(LazyMixin): + """ + Implements an Object which may be Blobs, Trees, Commits and Tags + """ + TYPES = ("blob", "tree", "commit", "tag") + __slots__ = ("repo", "id", "size", "data" ) + type = None # to be set by subclass + + def __init__(self, repo, id): + """ + Initialize an object by identifying it by its id. All keyword arguments + will be set on demand if None. + + ``repo`` + repository this object is located in + + ``id`` + SHA1 or ref suitable for git-rev-parse + """ + super(Object,self).__init__() + self.repo = repo + self.id = id + + def _set_self_from_args_(self, args_dict): + """ + Initialize attributes on self from the given dict that was retrieved + from locals() in the calling method. + + Will only set an attribute on self if the corresponding value in args_dict + is not None + """ + for attr, val in args_dict.items(): + if attr != "self" and val is not None: + setattr( self, attr, val ) + # END set all non-None attributes + + def _set_cache_(self, attr): + """ + Retrieve object information + """ + if attr == "size": + hexsha, typename, self.size = self.repo.git.get_object_header(self.id) + assert typename == self.type, _assertion_msg_format % (self.id, typename, self.type) + elif attr == "data": + hexsha, typename, self.size, self.data = self.repo.git.get_object_data(self.id) + assert typename == self.type, _assertion_msg_format % (self.id, typename, self.type) + else: + super(Object,self)._set_cache_(attr) + + def __eq__(self, other): + """ + Returns + True if the objects have the same SHA1 + """ + return self.id == other.id + + def __ne__(self, other): + """ + Returns + True if the objects do not have the same SHA1 + """ + return self.id != other.id + + def __hash__(self): + """ + Returns + Hash of our id allowing objects to be used in dicts and sets + """ + return hash(self.id) + + def __str__(self): + """ + Returns + string of our SHA1 as understood by all git commands + """ + return self.id + + def __repr__(self): + """ + Returns + string with pythonic representation of our object + """ + return '<git.%s "%s">' % (self.__class__.__name__, self.id) + + +class IndexObject(Object): + """ + Base for all objects that can be part of the index file , namely Tree, Blob and + SubModule objects + """ + __slots__ = ("path", "mode") + + def __init__(self, repo, id, mode=None, path=None): + """ + Initialize a newly instanced IndexObject + ``repo`` + is the Repo we are located in + + ``id`` : string + is the git object id as hex sha + + ``mode`` : int + is the file mode as int, use the stat module to evaluate the infomration + + ``path`` : str + is the path to the file in the file system, relative to the git repository root, i.e. + file.ext or folder/other.ext + + NOTE + Path may not be set of the index object has been created directly as it cannot + be retrieved without knowing the parent tree. + """ + super(IndexObject, self).__init__(repo, id) + self._set_self_from_args_(locals()) + if isinstance(mode, basestring): + self.mode = self._mode_str_to_int(mode) + + def _set_cache_(self, attr): + if attr in IndexObject.__slots__: + # they cannot be retrieved lateron ( not without searching for them ) + raise AttributeError( "path and mode attributes must have been set during %s object creation" % type(self).__name__ ) + else: + super(IndexObject, self)._set_cache_(attr) + + @classmethod + def _mode_str_to_int(cls, modestr): + """ + ``modestr`` + string like 755 or 644 or 100644 - only the last 3 chars will be used + + Returns + String identifying a mode compatible to the mode methods ids of the + stat module regarding the rwx permissions for user, group and other + """ + mode = 0 + for iteration,char in enumerate(reversed(modestr[-3:])): + mode += int(char) << iteration*3 + # END for each char + return mode + + diff --git a/lib/git/objects/blob.py b/lib/git/objects/blob.py new file mode 100644 index 00000000..88ca73d6 --- /dev/null +++ b/lib/git/objects/blob.py @@ -0,0 +1,36 @@ +# blob.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import mimetypes +import base + +class Blob(base.IndexObject): + """A Blob encapsulates a git blob object""" + DEFAULT_MIME_TYPE = "text/plain" + type = "blob" + + __slots__ = tuple() + + + @property + def mime_type(self): + """ + The mime type of this file (based on the filename) + + Returns + str + + NOTE + Defaults to 'text/plain' in case the actual file type is unknown. + """ + guesses = None + if self.path: + guesses = mimetypes.guess_type(self.path) + return guesses and guesses[0] or self.DEFAULT_MIME_TYPE + + + def __repr__(self): + return '<git.Blob "%s">' % self.id diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py new file mode 100644 index 00000000..101014ab --- /dev/null +++ b/lib/git/objects/commit.py @@ -0,0 +1,299 @@ +# commit.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import re +import time +from git.utils import Iterable +from git.actor import Actor +import git.diff as diff +import git.stats as stats +from tree import Tree +import base + +class Commit(base.Object, Iterable): + """ + Wraps a git Commit object. + + This class will act lazily on some of its attributes and will query the + value on demand only if it involves calling the git binary. + """ + # precompiled regex + re_actor_epoch = re.compile(r'^.+? (.*) (\d+) .*$') + + # object configuration + type = "commit" + __slots__ = ("tree", "author", "authored_date", "committer", "committed_date", + "message", "parents") + + def __init__(self, repo, id, tree=None, author=None, authored_date=None, + committer=None, committed_date=None, message=None, parents=None): + """ + Instantiate a new Commit. All keyword arguments taking None as default will + be implicitly set if id names a valid sha. + + The parameter documentation indicates the type of the argument after a colon ':'. + + ``id`` + is the sha id of the commit or a ref + + ``parents`` : tuple( Commit, ... ) + is a tuple of commit ids or actual Commits + + ``tree`` : Tree + is the corresponding tree id or an actual Tree + + ``author`` : Actor + is the author string ( will be implicitly converted into an Actor object ) + + ``authored_date`` : (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst ) + is the authored DateTime + + ``committer`` : Actor + is the committer string + + ``committed_date`` : (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst) + is the committed DateTime + + ``message`` : string + is the commit message + + Returns + git.Commit + """ + super(Commit,self).__init__(repo, id) + self._set_self_from_args_(locals()) + + if parents is not None: + self.parents = tuple( self.__class__(repo, p) for p in parents ) + # END for each parent to convert + + if self.id and tree is not None: + self.tree = Tree(repo, id=tree, path='') + # END id to tree conversion + + def _set_cache_(self, attr): + """ + Called by LazyMixin superclass when the given uninitialized member needs + to be set. + We set all values at once. + """ + if attr in Commit.__slots__: + # prepare our data lines to match rev-list + data_lines = self.data.splitlines() + data_lines.insert(0, "commit %s" % self.id) + temp = self._iter_from_process_or_stream(self.repo, iter(data_lines)).next() + 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 + else: + super(Commit, self)._set_cache_(attr) + + @property + def summary(self): + """ + Returns + First line of the commit message. + """ + return self.message.split('\n', 1)[0] + + @classmethod + def count(cls, repo, ref, path=''): + """ + Count the number of commits reachable from this ref + + ``repo`` + is the Repo + + ``ref`` + is the ref from which to begin (SHA1 or name) + + ``path`` + is an optinal path + + Returns + int + """ + return len(repo.git.rev_list(ref, '--', path).strip().splitlines()) + + @classmethod + def iter_items(cls, repo, ref, path='', **kwargs): + """ + Find all commits matching the given criteria. + + ``repo`` + is the Repo + + ``ref`` + is the ref from which to begin (SHA1, Head or name) + + ``path`` + is an optinal path, if set only Commits that include the path + will be considered + + ``kwargs`` + optional keyword arguments to git where + ``max_count`` is the maximum number of commits to fetch + ``skip`` is the number of commits to skip + + Returns + iterator yielding Commit items + """ + options = {'pretty': 'raw', 'as_process' : True } + options.update(kwargs) + + # the test system might confront us with string values - + proc = repo.git.rev_list(ref, '--', path, **options) + return cls._iter_from_process_or_stream(repo, proc) + + @classmethod + def _iter_from_process_or_stream(cls, repo, proc_or_stream): + """ + Parse out commit information into a list of Commit objects + + ``repo`` + is the Repo + + ``proc`` + git-rev-list process instance (raw format) + + Returns + iterator returning Commit objects + """ + stream = proc_or_stream + if not hasattr(stream,'next'): + stream = proc_or_stream.stdout + + for line in stream: + id = line.split()[1] + assert line.split()[0] == "commit" + tree = stream.next().split()[1] + + parents = [] + next_line = None + for parent_line in stream: + if not parent_line.startswith('parent'): + next_line = parent_line + break + # END abort reading parents + parents.append(parent_line.split()[-1]) + # END for each parent line + + author, authored_date = cls._actor(next_line) + committer, committed_date = cls._actor(stream.next()) + + # empty line + stream.next() + + message_lines = [] + next_line = None + for msg_line in stream: + if not msg_line.startswith(' '): + break + # END abort message reading + message_lines.append(msg_line.strip()) + # END while there are message lines + message = '\n'.join(message_lines) + + yield Commit(repo, id=id, parents=parents, tree=tree, author=author, authored_date=authored_date, + committer=committer, committed_date=committed_date, message=message) + # END for each line in stream + + @classmethod + def diff(cls, repo, a, b=None, paths=None): + """ + Creates diffs between a tree and the index or 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 to. + + Returns + git.Diff[]:: + + between tree and the index if only a is given + between two trees if a and b are given and are commits + """ + paths = paths or [] + + 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('-M', full_index=True, *paths) + return diff.Diff._list_from_string(repo, text) + + @property + def diffs(self): + """ + Returns + git.Diff[] + Diffs between this commit and its first parent or all changes if this + commit is the first commit and has no parent. + """ + if not self.parents: + d = self.repo.git.show(self.id, '-M', full_index=True, pretty='raw') + 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): + """ + Create a git stat from changes between this commit and its first parent + or from all changes done if this is the very first commit. + + Return + git.Stats + """ + if not self.parents: + text = self.repo.git.diff_tree(self.id, '--', numstat=True, root=True) + text2 = "" + for line in text.splitlines()[1:]: + (insertions, deletions, filename) = line.split("\t") + text2 += "%s\t%s\t%s\n" % (insertions, deletions, 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 '<git.Commit "%s">' % self.id + + @classmethod + def _actor(cls, line): + """ + Parse out the actor (author or committer) info + + Returns + [Actor, gmtime(acted at time)] + """ + m = cls.re_actor_epoch.search(line) + actor, epoch = m.groups() + return (Actor._from_string(actor), time.gmtime(int(epoch))) diff --git a/lib/git/objects/tag.py b/lib/git/objects/tag.py new file mode 100644 index 00000000..ecf6349d --- /dev/null +++ b/lib/git/objects/tag.py @@ -0,0 +1,70 @@ +# objects.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +""" +Module containing all object based types. +""" +import base +import commit +from utils import get_object_type_by_name + +class TagObject(base.Object): + """ + Non-Lightweight tag carrying additional information about an object we are pointing + to. + """ + type = "tag" + __slots__ = ( "object", "tag", "tagger", "tagged_date", "message" ) + + def __init__(self, repo, id, object=None, tag=None, + tagger=None, tagged_date=None, message=None): + """ + Initialize a tag object with additional data + + ``repo`` + repository this object is located in + + ``id`` + SHA1 or ref suitable for git-rev-parse + + ``object`` + Object instance of object we are pointing to + + ``tag`` + name of this tag + + ``tagger`` + Actor identifying the tagger + + ``tagged_date`` : (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst) + is the DateTime of the tag creation + """ + super(TagObject, self).__init__(repo, id ) + self._set_self_from_args_(locals()) + + def _set_cache_(self, attr): + """ + Cache all our attributes at once + """ + if attr in TagObject.__slots__: + lines = self.data.splitlines() + + obj, hexsha = lines[0].split(" ") # object <hexsha> + type_token, type_name = lines[1].split(" ") # type <type_name> + self.object = get_object_type_by_name(type_name)(self.repo, hexsha) + + self.tag = lines[2][4:] # tag <tag name> + + tagger_info = lines[3][7:]# tagger <actor> <date> + self.tagger, self.tagged_date = commit.Commit._actor(tagger_info) + + # line 4 empty - check git source to figure out purpose + self.message = "\n".join(lines[5:]) + # END check our attributes + else: + super(TagObject, self)._set_cache_(attr) + + + diff --git a/lib/git/objects/tree.py b/lib/git/objects/tree.py new file mode 100644 index 00000000..abfa9622 --- /dev/null +++ b/lib/git/objects/tree.py @@ -0,0 +1,242 @@ +# tree.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import os +import blob +import base +import binascii + +def sha_to_hex(sha): + """Takes a string and returns the hex of the sha within""" + hexsha = binascii.hexlify(sha) + assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % hexsha + return hexsha + +class Tree(base.IndexObject): + """ + Tress represent a ordered list of Blobs and other Trees. Hence it can be + accessed like a list. + + Tree's will cache their contents after first retrieval to improve efficiency. + + ``Tree as a list``:: + + Access a specific blob using the + tree['filename'] notation. + + You may as well access by index + blob = tree[0] + + + """ + + type = "tree" + __slots__ = "_cache" + + # using ascii codes for comparison + ascii_commit_id = (0x31 << 4) + 0x36 + ascii_blob_id = (0x31 << 4) + 0x30 + ascii_tree_id = (0x34 << 4) + 0x30 + + + def __init__(self, repo, id, mode=0, path=None): + super(Tree, self).__init__(repo, id, mode, path) + + def _set_cache_(self, attr): + if attr == "_cache": + # Set the data when we need it + self._cache = self._get_tree_cache() + else: + super(Tree, self)._set_cache_(attr) + + def _get_tree_cache(self): + """ + Return + list(object_instance, ...) + + ``treeish`` + sha or ref identifying a tree + """ + out = list() + for obj in self._iter_from_data(): + if obj is not None: + out.append(obj) + # END if object was handled + # END for each line from ls-tree + return out + + + def _iter_from_data(self): + """ + Reads the binary non-pretty printed representation of a tree and converts + it into Blob, Tree or Commit objects. + + Note: This method was inspired by the parse_tree method in dulwich. + + Returns + list(IndexObject, ...) + """ + ord_zero = ord('0') + data = self.data + len_data = len(data) + i = 0 + while i < len_data: + mode = 0 + mode_boundary = i + 6 + + # keep it ascii - we compare against the respective values + type_id = (ord(data[i])<<4) + ord(data[i+1]) + i += 2 + + while data[i] != ' ': + # move existing mode integer up one level being 3 bits + # and add the actual ordinal value of the character + mode = (mode << 3) + (ord(data[i]) - ord_zero) + i += 1 + # END while reading mode + + # byte is space now, skip it + i += 1 + + # parse name, it is NULL separated + + ns = i + while data[i] != '\0': + i += 1 + # END while not reached NULL + name = data[ns:i] + + # byte is NULL, get next 20 + i += 1 + sha = data[i:i+20] + i = i + 20 + + hexsha = sha_to_hex(sha) + if type_id == self.ascii_blob_id: + yield blob.Blob(self.repo, hexsha, mode, name) + elif type_id == self.ascii_tree_id: + yield Tree(self.repo, hexsha, mode, name) + elif type_id == self.ascii_commit_id: + # todo + yield None + else: + raise TypeError( "Unknown type found in tree data: %i" % type_id ) + # END for each byte in data stream + + + def __div__(self, file): + """ + Find the named object in this tree's contents + + Examples:: + + >>> Repo('/path/to/python-git').tree/'lib' + <git.Tree "6cc23ee138be09ff8c28b07162720018b244e95e"> + >>> Repo('/path/to/python-git').tree/'README.txt' + <git.Blob "8b1e02c0fb554eed2ce2ef737a68bb369d7527df"> + + Returns + ``git.Blob`` or ``git.Tree`` + + Raise + KeyError if given file or tree does not exist in tree + """ + return self[file] + + + def __repr__(self): + return '<git.Tree "%s">' % self.id + + @classmethod + def _iter_recursive(cls, repo, tree, cur_depth, max_depth, predicate ): + + for obj in tree: + # adjust path to be complete + obj.path = os.path.join(tree.path, obj.path) + if not predicate(obj): + continue + yield obj + if obj.type == "tree" and ( max_depth < 0 or cur_depth+1 <= max_depth ): + for recursive_obj in cls._iter_recursive( repo, obj, cur_depth+1, max_depth, predicate ): + yield recursive_obj + # END for each recursive object + # END if we may enter recursion + # END for each object + + def traverse(self, max_depth=-1, predicate = lambda i: True): + """ + Returns + Iterator to traverse the tree recursively up to the given level. + The iterator returns Blob and Tree objects + + ``max_depth`` + + if -1, the whole tree will be traversed + if 0, only the first level will be traversed which is the same as + the default non-recursive iterator + + ``predicate`` + + If predicate(item) returns True, item will be returned by iterator + """ + return self._iter_recursive( self.repo, self, 0, max_depth, predicate ) + + @property + def trees(self): + """ + Returns + list(Tree, ...) list of trees directly below this tree + """ + return [ i for i in self if i.type == "tree" ] + + @property + def blobs(self): + """ + Returns + list(Blob, ...) list of blobs directly below this tree + """ + return [ i for i in self if i.type == "blob" ] + + + # List protocol + def __getslice__(self,i,j): + return self._cache[i:j] + + def __iter__(self): + return iter(self._cache) + + def __len__(self): + return len(self._cache) + + def __getitem__(self,item): + if isinstance(item, int): + return self._cache[item] + + if isinstance(item, basestring): + # compatability + for obj in self._cache: + if obj.path == item: + return obj + # END for each obj + raise KeyError( "Blob or Tree named %s not found" % item ) + # END index is basestring + + raise TypeError( "Invalid index type: %r" % item ) + + + def __contains__(self,item): + if isinstance(item, base.IndexObject): + return item in self._cache + + # compatability + for obj in self._cache: + if item == obj.path: + return True + # END for each item + return False + + def __reversed__(self): + return reversed(self._cache) diff --git a/lib/git/objects/utils.py b/lib/git/objects/utils.py new file mode 100644 index 00000000..15c1d114 --- /dev/null +++ b/lib/git/objects/utils.py @@ -0,0 +1,36 @@ +# util.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +""" +Module for general utility functions +""" +import commit, tag, blob, tree + +def get_object_type_by_name(object_type_name): + """ + Returns + type suitable to handle the given object type name. + Use the type to create new instances. + + ``object_type_name`` + Member of TYPES + + Raises + ValueError: In case object_type_name is unknown + """ + if object_type_name == "commit": + import commit + return commit.Commit + elif object_type_name == "tag": + import tag + return tag.TagObject + elif object_type_name == "blob": + import blob + return blob.Blob + elif object_type_name == "tree": + import tree + return tree.Tree + else: + raise ValueError("Cannot handle unknown object type: %s" % object_type_name) diff --git a/lib/git/refs.py b/lib/git/refs.py new file mode 100644 index 00000000..3c9eb817 --- /dev/null +++ b/lib/git/refs.py @@ -0,0 +1,238 @@ +# refs.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +""" +Module containing all ref based objects +""" +from objects.base import Object +from objects.utils import get_object_type_by_name +from utils import LazyMixin, Iterable + +class Ref(LazyMixin, Iterable): + """ + Represents a named reference to any object + """ + __slots__ = ("repo", "path") + + def __init__(self, repo, path, object = None): + """ + Initialize this instance + ``repo`` + Our parent repository + + ``path`` + Path relative to the .git/ directory pointing to the ref in question, i.e. + refs/heads/master + + ``object`` + Object instance, will be retrieved on demand if None + """ + self.repo = repo + self.path = path + if object is not None: + self.object = object + + def __str__(self): + return self.name + + def __repr__(self): + return '<git.%s "%s">' % (self.__class__.__name__, self.path) + + def __eq__(self, other): + return self.path == other.path and self.object == other.object + + def __ne__(self, other): + return not ( self == other ) + + def __hash__(self): + return hash(self.path) + + @property + def name(self): + """ + Returns + (shortest) Name of this reference - it may contain path components + """ + # first two path tokens are can be removed as they are + # refs/heads or refs/tags or refs/remotes + tokens = self.path.split('/') + if len(tokens) < 3: + return self.path # could be refs/HEAD + + return '/'.join(tokens[2:]) + + @property + def object(self): + """ + Returns + The object our ref currently refers to. Refs can be cached, they will + always point to the actual object as it gets re-created on each query + """ + # have to be dynamic here as we may be a tag which can point to anything + hexsha, typename, size = self.repo.git.get_object_header(self.path) + return get_object_type_by_name(typename)(self.repo, hexsha) + + @classmethod + def iter_items(cls, repo, common_path = "refs", **kwargs): + """ + Find all refs in the repository + + ``repo`` + is the Repo + + ``common_path`` + Optional keyword argument to the path which is to be shared by all + returned Ref objects + + ``kwargs`` + Additional options given as keyword arguments, will be passed + to git-for-each-ref + + Returns + git.Ref[] + + List is sorted by committerdate + The returned objects are compatible to the Ref base, but represent the + actual type, such as Head or Tag + """ + + options = {'sort': "committerdate", + 'format': "%(refname)%00%(objectname)%00%(objecttype)%00%(objectsize)"} + + options.update(kwargs) + + output = repo.git.for_each_ref(common_path, **options) + return cls._iter_from_stream(repo, iter(output.splitlines())) + + @classmethod + def _iter_from_stream(cls, repo, stream): + """ Parse out ref information into a list of Ref compatible objects + Returns git.Ref[] list of Ref objects """ + heads = [] + + for line in stream: + heads.append(cls._from_string(repo, line)) + + return heads + + @classmethod + def _from_string(cls, repo, line): + """ Create a new Ref instance from the given string. + Format + name: [a-zA-Z_/]+ + <null byte> + id: [0-9A-Fa-f]{40} + Returns git.Head """ + full_path, hexsha, type_name, object_size = line.split("\x00") + + # No, we keep the object dynamic by allowing it to be retrieved by + # our path on demand - due to perstent commands it is fast. + # This reduces the risk that the object does not match + # the changed ref anymore in case it changes in the meanwhile + return cls(repo, full_path) + + # obj = get_object_type_by_name(type_name)(repo, hexsha) + # obj.size = object_size + # return cls(repo, full_path, obj) + + +class Head(Ref): + """ + 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 + <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> + + >>> head.commit.id + '1c09f116cbc2cb4100fb6935bb162daa4723f455' + """ + + @property + def commit(self): + """ + Returns + Commit object the head points to + """ + return self.object + + @classmethod + def iter_items(cls, repo, common_path = "refs/heads", **kwargs): + """ + Returns + Iterator yielding Head items + + For more documentation, please refer to git.base.Ref.list_items + """ + return super(Head,cls).iter_items(repo, common_path, **kwargs) + + def __repr__(self): + return '<git.Head "%s">' % self.name + + + +class TagRef(Ref): + """ + Class representing a lightweight tag reference which either points to a commit + or to a tag object. In the latter case additional information, like the signature + or the tag-creator, is available. + + This tag object will always point to a commit object, but may carray additional + information in a tag object:: + + tagref = TagRef.list_items(repo)[0] + print tagref.commit.message + if tagref.tag is not None: + print tagref.tag.message + """ + + __slots__ = tuple() + + @property + def commit(self): + """ + Returns + Commit object the tag ref points to + """ + if self.object.type == "commit": + return self.object + elif self.object.type == "tag": + # it is a tag object which carries the commit as an object - we can point to anything + return self.object.object + else: + raise ValueError( "Tag %s points to a Blob or Tree - have never seen that before" % self ) + + @property + def tag(self): + """ + Returns + Tag object this tag ref points to or None in case + we are a light weight tag + """ + if self.object.type == "tag": + return self.object + return None + + @classmethod + def iter_items(cls, repo, common_path = "refs/tags", **kwargs): + """ + Returns + Iterator yielding commit items + + For more documentation, please refer to git.base.Ref.list_items + """ + return super(TagRef,cls).iter_items(repo, common_path, **kwargs) + + +# provide an alias +Tag = TagRef diff --git a/lib/git/repo.py b/lib/git/repo.py index 1c4b4095..c74c7e8d 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -8,506 +8,588 @@ import os import re import gzip import StringIO +import time + from errors import InvalidGitRepositoryError, NoSuchPathError from utils import touch, is_git_dir from cmd import Git -from head import Head -from blob import Blob -from tag import Tag -from commit import Commit -from tree import Tree +from actor import Actor +from refs import * +from objects import * + class Repo(object): - """ - Represents a git repository and allows you to query references, - gather commit information, generate diffs, create and clone repositories query - the log. - """ - DAEMON_EXPORT_FILE = 'git-daemon-export-ok' - - def __init__(self, path=None): - """ - 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") - - Raises - InvalidGitRepositoryError or NoSuchPathError - - Returns - ``git.Repo`` - """ - - epath = os.path.abspath(os.path.expanduser(path or os.getcwd())) - - if not os.path.exists(epath): - raise NoSuchPathError(epath) - - self.path = None - curpath = epath - while curpath: - if is_git_dir(curpath): - self.bare = True - self.path = curpath - self.wd = curpath - break - gitpath = os.path.join(curpath, '.git') - if is_git_dir(gitpath): - self.bare = False - self.path = gitpath - self.wd = curpath - break - curpath, dummy = os.path.split(curpath) - if not dummy: - break - - if self.path is None: - raise InvalidGitRepositoryError(epath) - - self.git = Git(self.wd) - - # Description property - def _get_description(self): - filename = os.path.join(self.path, 'description') - return file(filename).read().rstrip() - - def _set_description(self, descr): - filename = os.path.join(self.path, 'description') - file(filename, 'w').write(descr+'\n') - - description = property(_get_description, _set_description, - doc="the project's description") - del _get_description - del _set_description - - @property - def heads(self): - """ - A list of ``Head`` objects representing the branch heads in - this repo - - Returns - ``git.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 - ``git.Tag[]`` - """ - return Tag.find_all(self) - - def commits(self, start='master', path='', 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') - - ``path`` - is an optional path to limit the returned commits to - Commits that do not contain that path will not be returned. - - ``max_count`` - is the maximum number of commits to return (default 10) + """ + Represents a git repository and allows you to query references, + gather commit information, generate diffs, create and clone repositories query + the log. + """ + DAEMON_EXPORT_FILE = 'git-daemon-export-ok' + + # precompiled regex + re_whitespace = re.compile(r'\s+') + re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') + re_author_committer_start = re.compile(r'^(author|committer)') + re_tab_full_line = re.compile(r'^\t(.*)$') + + def __init__(self, path=None): + """ + 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") + + Raises + InvalidGitRepositoryError or NoSuchPathError + + Returns + ``git.Repo`` + """ + + epath = os.path.abspath(os.path.expanduser(path or os.getcwd())) + + if not os.path.exists(epath): + raise NoSuchPathError(epath) + + self.path = None + curpath = epath + while curpath: + if is_git_dir(curpath): + self.bare = True + self.path = curpath + self.wd = curpath + break + gitpath = os.path.join(curpath, '.git') + if is_git_dir(gitpath): + self.bare = False + self.path = gitpath + self.wd = curpath + break + curpath, dummy = os.path.split(curpath) + if not dummy: + break + + if self.path is None: + raise InvalidGitRepositoryError(epath) + + self.git = Git(self.wd) + + # Description property + def _get_description(self): + filename = os.path.join(self.path, 'description') + return file(filename).read().rstrip() + + def _set_description(self, descr): + filename = os.path.join(self.path, 'description') + file(filename, 'w').write(descr+'\n') + + description = property(_get_description, _set_description, + doc="the project's description") + del _get_description + del _set_description + + @property + def heads(self): + """ + A list of ``Head`` objects representing the branch heads in + this repo + + Returns + ``git.Head[]`` + """ + return Head.list_items(self) + + # alias heads + branches = heads + + @property + def tags(self): + """ + A list of ``Tag`` objects that are available in this repo + + Returns + ``git.Tag[]`` + """ + return Tag.list_items(self) + + def blame(self, commit, file): + """ + The blame information for the given file at the given commit + + Returns + list: [git.Commit, list: [<line>]] + A list of tuples associating a Commit object with a list of lines that + changed within the given commit. The Commit objects will be given in order + of appearance. + """ + data = self.git.blame(commit, '--', file, p=True) + commits = {} + blames = [] + info = None + + for line in data.splitlines(False): + parts = self.re_whitespace.split(line, 1) + firstpart = parts[0] + if self.re_hexsha_only.search(firstpart): + # handles + # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start + # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 + digits = parts[-1].split(" ") + if len(digits) == 3: + info = {'id': firstpart} + blames.append([None, []]) + # END blame data initialization + else: + m = self.re_author_committer_start.search(firstpart) + if m: + # handles: + # 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 - IGNORED BY US + role = m.group(0) + if firstpart.endswith('-mail'): + info["%s_email" % role] = parts[-1] + elif firstpart.endswith('-time'): + info["%s_date" % role] = time.gmtime(int(parts[-1])) + elif role == firstpart: + info[role] = parts[-1] + # END distinguish mail,time,name + else: + # handle + # filename lib/grit.rb + # summary add Blob + # <and rest> + if firstpart.startswith('filename'): + info['filename'] = parts[-1] + elif firstpart.startswith('summary'): + info['summary'] = parts[-1] + elif firstpart == '': + if info: + sha = info['id'] + c = commits.get(sha) + if c is None: + c = Commit( self, id=sha, + 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[sha] = c + # END if commit objects needs initial creation + m = self.re_tab_full_line.search(line) + text, = m.groups() + blames[-1][0] = c + blames[-1][1].append( text ) + info = None + # END if we collected commit info + # END distinguish filename,summary,rest + # END distinguish author|committer vs filename,summary,rest + # END distinguish hexsha vs other information + return blames + + def commits(self, start='master', path='', max_count=None, skip=0): + """ + A list of Commit objects representing the history of a given ref/commit + + ``start`` + is the branch/commit name (default 'master') + + ``path`` + is an optional path to limit the returned commits to + Commits that do not contain that path will not be returned. + + ``max_count`` + is the maximum number of commits to return (default None) + + ``skip`` + is the number of commits to skip (default 0) which will effectively + move your commit-window by the given number. + + Returns + ``git.Commit[]`` + """ + options = {'max_count': max_count, + 'skip': skip} + + if max_count is None: + options.pop('max_count') + + return Commit.list_items(self, start, path, **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 + ``git.Commit[]`` + """ + return reversed(Commit.list_items(self, "%s..%s" % (frm, to))) + + def commits_since(self, start='master', path='', 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') + + ``path`` + is an optinal path to limit the returned commits to. + + + ``since`` + is a string represeting a date/time - ``skip`` - is the number of commits to skip (default 0) which will effectively - move your commit-window by the given number. + Returns + ``git.Commit[]`` + """ + options = {'since': since} - Returns - ``git.Commit[]`` - """ - options = {'max_count': max_count, - 'skip': skip} + return Commit.list_items(self, start, path, **options) - return Commit.find_all(self, start, path, **options) + def commit_count(self, start='master', path=''): + """ + The number of commits reachable by the given branch/commit - def commits_between(self, frm, to): - """ - The Commits objects that are reachable via ``to`` but not via ``frm`` - Commits are returned in chronological order. + ``start`` + is the branch/commit name (default 'master') - ``from`` - is the branch/commit name of the younger item + ``path`` + is an optional path + Commits that do not contain the path will not contribute to the count. - ``to`` - is the branch/commit name of the older item - - Returns - ``git.Commit[]`` - """ - return reversed(Commit.find_all(self, "%s..%s" % (frm, to))) - - def commits_since(self, start='master', path='', since='1970-01-01'): - """ - The Commits objects that are newer than the specified date. - Commits are returned in chronological order. + Returns + ``int`` + """ + return Commit.count(self, start, path) - ``start`` - is the branch/commit name (default 'master') + def commit(self, id=None, path = ''): + """ + The Commit object for the specified id - ``path`` - is an optinal path to limit the returned commits to. - + ``id`` + is the SHA1 identifier of the commit or a ref or a ref name + if None, it defaults to the active branch + - ``since`` - is a string represeting a date/time + ``path`` + is an optional path, if set the returned commit must contain the path. - Returns - ``git.Commit[]`` - """ - options = {'since': since} + Returns + ``git.Commit`` + """ + if id is None: + id = self.active_branch + options = {'max_count': 1} - return Commit.find_all(self, start, path, **options) - - def commit_count(self, start='master', path=''): - """ - The number of commits reachable by the given branch/commit - - ``start`` - is the branch/commit name (default 'master') + commits = Commit.list_items(self, id, path, **options) - ``path`` - is an optional path - Commits that do not contain the path will not contribute to the count. - - Returns - ``int`` - """ - return Commit.count(self, start, path) - - def commit(self, id, path = ''): - """ - The Commit object for the specified id - - ``id`` - is the SHA1 identifier of the commit - - ``path`` - is an optional path, if set the returned commit must contain the path. - - Returns - ``git.Commit`` - """ - options = {'max_count': 1} - - commits = Commit.find_all(self, id, path, **options) - - if not commits: - raise ValueError, "Invalid identifier %s, or given path '%s' too restrictive" % ( id, path ) - return commits[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 - git.Commit[] - """ - repo_refs = self.git.rev_list(ref, '--').strip().splitlines() - other_repo_refs = other_repo.git.rev_list(other_ref, '--').strip().splitlines() + if not commits: + raise ValueError, "Invalid identifier %s, or given path '%s' too restrictive" % ( id, path ) + return commits[0] - 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 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 - def tree(self, treeish='master'): - """ - The Tree object for the given treeish reference + Returns + git.Commit[] + """ + repo_refs = self.git.rev_list(ref, '--').strip().splitlines() + other_repo_refs = other_repo.git.rev_list(other_ref, '--').strip().splitlines() - ``treeish`` - is the reference (default 'master') + diff_refs = list(set(other_repo_refs) - set(repo_refs)) + return map(lambda ref: Commit.list_items(other_repo, ref, max_count=1)[0], diff_refs) - Examples:: + def tree(self, treeish=None): + """ + The Tree object for the given treeish reference - repo.tree('master') + ``treeish`` + is a Ref instance defaulting to the active_branch if None. + Examples:: + + repo.tree(repo.heads[0]) + + Returns + ``git.Tree`` + + NOTE + A ref is requried here to assure you point to a commit or tag. Otherwise + it is not garantueed that you point to the root-level tree. + + If you need a non-root level tree, find it by iterating the root tree. + """ + if treeish is None: + treeish = self.active_branch + if not isinstance(treeish, Ref): + raise ValueError( "Treeish reference required, got %r" % treeish ) + + + # As we are directly reading object information, we must make sure + # we truly point to a tree object. We resolve the ref to a sha in all cases + # to assure the returned tree can be compared properly. Except for + # heads, ids should always be hexshas + hexsha, typename, size = self.git.get_object_header( treeish ) + if typename != "tree": + hexsha, typename, size = self.git.get_object_header( str(treeish)+'^{tree}' ) + # END tree handling + treeish = hexsha + + # the root has an empty relative path and the default mode + return Tree(self, treeish, 0, '') + + + 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 + + Returns + ``str`` + """ + return self.git.diff(a, b, '--', *paths) - Returns - ``git.Tree`` - """ - return Tree(self, id=treeish) + def commit_diff(self, commit): + """ + The commit diff for the given commit + ``commit`` is the commit name/id - def blob(self, id): - """ - The Blob object for the given id - - ``id`` - is the SHA1 id of the blob - - Returns - ``git.Blob`` - """ - return Blob(self, id=id) - - def log(self, commit='master', path=None, **kwargs): - """ - The Commit for a treeish, and all commits leading to it. - - ``kwargs`` - keyword arguments specifying flags to be used in git-log command, - i.e.: max_count=1 to limit the amount of commits returned + Returns + ``git.Diff[]`` + """ + return Commit.diff(self, commit) - Returns - ``git.Commit[]`` - """ - options = {'pretty': 'raw'} - options.update(kwargs) - arg = [commit, '--'] - if path: - arg.append(path) - commits = self.git.log(*arg, **options) - return Commit.list_from_string(self, commits) + @classmethod + def init_bare(self, path, mkdir=True, **kwargs): + """ + Initialize a bare git repository at the given path - 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 + ``path`` + is the full path to the repo (traditionally ends with /<name>.git) - ``paths`` - is an optional list of file paths on which to restrict the diff - - Returns - ``str`` - """ - return self.git.diff(a, b, '--', *paths) + ``mkdir`` + if specified will create the repository directory if it doesn't + already exists. Creates the directory with a mode=0755. - def commit_diff(self, commit): - """ - The commit diff for the given commit - ``commit`` is the commit name/id + ``kwargs`` + keyword arguments serving as additional options to the git init command - Returns - ``git.Diff[]`` - """ - return Commit.diff(self, commit) + Examples:: - @classmethod - def init_bare(self, path, mkdir=True, **kwargs): - """ - Initialize a bare git repository at the given path + git.Repo.init_bare('/var/git/myrepo.git') - ``path`` - is the full path to the repo (traditionally ends with /<name>.git) - - ``mkdir`` - if specified will create the repository directory if it doesn't - already exists. Creates the directory with a mode=0755. - - ``kwargs`` - keyword arguments serving as additional options to the git init command - - Examples:: - - git.Repo.init_bare('/var/git/myrepo.git') - - Returns - ``git.Repo`` (the newly created repo) - """ - - if mkdir and not os.path.exists(path): - os.makedirs(path, 0755) - - git = Git(path) - output = git.init('--bare', **kwargs) - return Repo(path) - create = init_bare - - 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) - - ``kwargs`` - keyword arguments to be given to the git clone command - - Returns - ``git.Repo`` (the newly forked repo) - """ - options = {'bare': True} - 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 to prepend to each filename in the archive - - 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 bytes archive, whose files are prefixed with 'myproject/'> - - Returns - str (containing bytes of 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 to prepend to each filename in the archive - - 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 the bytes of tar.gz archive) - """ - kwargs = {} - if prefix: - kwargs['prefix'] = prefix - resultstr = self.git.archive(treeish, **kwargs) - sio = StringIO.StringIO() - gf = gzip.GzipFile(fileobj=sio, mode ='wb') - gf.write(resultstr) - gf.close() - return sio.getvalue() - - def _get_daemon_export(self): - filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) - return os.path.exists(filename) - - def _set_daemon_export(self, value): - filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) - fileexists = os.path.exists(filename) - if value and not fileexists: - touch(filename) - elif not value and fileexists: - os.unlink(filename) - - daemon_export = property(_get_daemon_export, _set_daemon_export, - doc="If True, git-daemon may export this repository") - del _get_daemon_export - del _set_daemon_export - - def _get_alternates(self): - """ - The list of alternates for this repo from which objects can be retrieved - - Returns - list of strings being 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 at which - git should look for objects, i.e. /home/user/repo/.git/objects + Returns + ``git.Repo`` (the newly created repo) + """ + + if mkdir and not os.path.exists(path): + os.makedirs(path, 0755) + + git = Git(path) + output = git.init('--bare', **kwargs) + return Repo(path) + create = init_bare + + 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) + + ``kwargs`` + keyword arguments to be given to the git clone command + + Returns + ``git.Repo`` (the newly forked repo) + """ + options = {'bare': True} + 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 to prepend to each filename in the archive + + 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 bytes archive, whose files are prefixed with 'myproject/'> + + Returns + str (containing bytes of 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 to prepend to each filename in the archive + + 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 the bytes of tar.gz archive) + """ + kwargs = {} + if prefix: + kwargs['prefix'] = prefix + resultstr = self.git.archive(treeish, **kwargs) + sio = StringIO.StringIO() + gf = gzip.GzipFile(fileobj=sio, mode ='wb') + gf.write(resultstr) + gf.close() + return sio.getvalue() + + def _get_daemon_export(self): + filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) + return os.path.exists(filename) + + def _set_daemon_export(self, value): + filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) + fileexists = os.path.exists(filename) + if value and not fileexists: + touch(filename) + elif not value and fileexists: + os.unlink(filename) + + daemon_export = property(_get_daemon_export, _set_daemon_export, + doc="If True, git-daemon may export this repository") + del _get_daemon_export + del _set_daemon_export + + def _get_alternates(self): + """ + The list of alternates for this repo from which objects can be retrieved + + Returns + list of strings being 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 at which + git should look for objects, i.e. /home/user/repo/.git/objects Raises NoSuchPathError - 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, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates") - - @property - def is_dirty(self): - """ - Return the status of the index. - - Returns - ``True``, if the index has any uncommitted changes, - otherwise ``False`` + 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, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates") + + @property + def is_dirty(self): + """ + Return the status of the index. + + Returns + ``True``, if the index has any uncommitted changes, + otherwise ``False`` NOTE Working tree changes that have not been staged will not be detected ! - """ - if self.bare: - # Bare repositories with no associated working directory are - # always consired to be clean. - return False - - return len(self.git.diff('HEAD', '--').strip()) > 0 - - @property - def active_branch(self): - """ - The name of the currently active branch. - - Returns - str (the branch name) - """ - branch = self.git.symbolic_ref('HEAD').strip() - if branch.startswith('refs/heads/'): - branch = branch[len('refs/heads/'):] - - return branch - - def __repr__(self): - return '<git.Repo "%s">' % self.path + """ + if self.bare: + # Bare repositories with no associated working directory are + # always consired to be clean. + return False + + return len(self.git.diff('HEAD', '--').strip()) > 0 + + @property + def active_branch(self): + """ + The name of the currently active branch. + + Returns + Head to the active branch + """ + return Head( self, self.git.symbolic_ref('HEAD').strip() ) + + def __repr__(self): + return '<git.Repo "%s">' % self.path diff --git a/lib/git/stats.py b/lib/git/stats.py index 307e2f2f..bda4e539 100644 --- a/lib/git/stats.py +++ b/lib/git/stats.py @@ -5,55 +5,56 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php class Stats(object): - """ - Represents stat information as presented by git at the end of a merge. It is - created from the output of a diff operation. - - ``Example``:: - - c = Commit( sha1 ) - s = c.stats - s.total # full-stat-dict - s.files # dict( filepath : stat-dict ) - - ``stat-dict`` - - A dictionary with the following keys and values:: - - deletions = number of deleted lines as int - insertions = number of inserted lines as int - lines = total number of lines changed as int, or deletions + insertions - - ``full-stat-dict`` - - In addition to the items in the stat-dict, it features additional information:: - - files = number of changed files as int - - """ - def __init__(self, repo, total, files): - self.repo = repo - self.total = total - self.files = files + """ + Represents stat information as presented by git at the end of a merge. It is + created from the output of a diff operation. + + ``Example``:: + + c = Commit( sha1 ) + s = c.stats + s.total # full-stat-dict + s.files # dict( filepath : stat-dict ) + + ``stat-dict`` + + A dictionary with the following keys and values:: + + deletions = number of deleted lines as int + insertions = number of inserted lines as int + lines = total number of lines changed as int, or deletions + insertions + + ``full-stat-dict`` + + In addition to the items in the stat-dict, it features additional information:: + + files = number of changed files as int + + """ + __slots__ = ("total", "files") + + def __init__(self, total, files): + self.total = total + self.files = files - @classmethod - def list_from_string(cls, repo, text): - """ - Create a Stat object from output retrieved by git-diff. - - Returns - git.Stat - """ - hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': {}} - for line in text.splitlines(): - (raw_insertions, raw_deletions, filename) = line.split("\t") - insertions = raw_insertions != '-' and int(raw_insertions) or 0 - deletions = raw_deletions != '-' and int(raw_deletions) or 0 - hsh['total']['insertions'] += insertions - hsh['total']['deletions'] += deletions - hsh['total']['lines'] += insertions + deletions - hsh['total']['files'] += 1 - hsh['files'][filename.strip()] = {'insertions': insertions, - 'deletions': deletions, - 'lines': insertions + deletions} - return Stats(repo, hsh['total'], hsh['files']) + @classmethod + def _list_from_string(cls, repo, text): + """ + Create a Stat object from output retrieved by git-diff. + + Returns + git.Stat + """ + hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': {}} + for line in text.splitlines(): + (raw_insertions, raw_deletions, filename) = line.split("\t") + insertions = raw_insertions != '-' and int(raw_insertions) or 0 + deletions = raw_deletions != '-' and int(raw_deletions) or 0 + hsh['total']['insertions'] += insertions + hsh['total']['deletions'] += deletions + hsh['total']['lines'] += insertions + deletions + hsh['total']['files'] += 1 + hsh['files'][filename.strip()] = {'insertions': insertions, + 'deletions': deletions, + 'lines': insertions + deletions} + return Stats(hsh['total'], hsh['files']) diff --git a/lib/git/tag.py b/lib/git/tag.py deleted file mode 100644 index 8413ce73..00000000 --- a/lib/git/tag.py +++ /dev/null @@ -1,92 +0,0 @@ -# tag.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -from commit import Commit - -class Tag(object): - def __init__(self, name, commit): - """ - Initialize a newly instantiated Tag - - ``name`` - is the name of the head - - ``commit`` - is the Commit that the head points to - """ - self.name = name - self.commit = commit - - @classmethod - def find_all(cls, repo, **kwargs): - """ - Find all Tags in the repository - - ``repo`` - is the Repo - - ``kwargs`` - Additional options given as keyword arguments, will be passed - to git-for-each-ref - - Returns - ``git.Tag[]`` - - List is sorted by committerdate - """ - 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 Tag objects - - ``repo`` - is the Repo - - ``text`` - is the text output from the git-for-each command - - Returns - git.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 - git.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 '<git.Tag "%s">' % self.name diff --git a/lib/git/tree.py b/lib/git/tree.py deleted file mode 100644 index cfb0881c..00000000 --- a/lib/git/tree.py +++ /dev/null @@ -1,108 +0,0 @@ -# tree.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -import os -from lazy import LazyMixin -import blob - -class Tree(LazyMixin): - def __init__(self, repo, id, mode=None, name=None): - LazyMixin.__init__(self) - self.repo = repo - self.id = id - self.mode = mode - self.name = name - self._contents = None - - def __bake__(self): - # Ensure the treeish references directly a tree - treeish = self.id - if not treeish.endswith(':'): - treeish = treeish + ':' - - # Read the tree contents. - self._contents = {} - for line in self.repo.git.ls_tree(self.id).splitlines(): - obj = self.content_from_string(self.repo, line) - if obj is not None: - self._contents[obj.name] = obj - - @staticmethod - def content_from_string(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 - ``git.Blob`` or ``git.Tree`` - """ - try: - mode, typ, id, name = text.expandtabs(1).split(" ", 3) - 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' - <git.Tree "6cc23ee138be09ff8c28b07162720018b244e95e"> - >>> Repo('/path/to/python-git').tree/'README.txt' - <git.Blob "8b1e02c0fb554eed2ce2ef737a68bb369d7527df"> - - Returns - ``git.Blob`` or ``git.Tree`` or ``None`` if not found - """ - return self.get(file) - - @property - def basename(self): - os.path.basename(self.name) - - def __repr__(self): - return '<git.Tree "%s">' % self.id - - # Implement the basics of the dict protocol: - # directories/trees can be seen as object dicts. - def __getitem__(self, key): - return self._contents[key] - - def __iter__(self): - return iter(self._contents) - - def __len__(self): - return len(self._contents) - - def __contains__(self, key): - return key in self._contents - - def get(self, key): - return self._contents.get(key) - - def items(self): - return self._contents.items() - - def keys(self): - return self._contents.keys() - - def values(self): - return self._contents.values() diff --git a/lib/git/utils.py b/lib/git/utils.py index 5d0ba8ca..f84c247d 100644 --- a/lib/git/utils.py +++ b/lib/git/utils.py @@ -7,20 +7,83 @@ import os def dashify(string): - return string.replace('_', '-') + return string.replace('_', '-') def touch(filename): - os.utime(filename) + os.utime(filename) def is_git_dir(d): - """ This is taken from the git setup.c:is_git_directory - function.""" - - if os.path.isdir(d) and \ - os.path.isdir(os.path.join(d, 'objects')) and \ - os.path.isdir(os.path.join(d, 'refs')): - headref = os.path.join(d, 'HEAD') - return os.path.isfile(headref) or \ - (os.path.islink(headref) and - os.readlink(headref).startswith('refs')) - return False + """ This is taken from the git setup.c:is_git_directory + function.""" + + if os.path.isdir(d) and \ + os.path.isdir(os.path.join(d, 'objects')) and \ + os.path.isdir(os.path.join(d, 'refs')): + headref = os.path.join(d, 'HEAD') + return os.path.isfile(headref) or \ + (os.path.islink(headref) and + os.readlink(headref).startswith('refs')) + return False + + +class LazyMixin(object): + """ + Base class providing an interface to lazily retrieve attribute values upon + first access. If slots are used, memory will only be reserved once the attribute + is actually accessed and retrieved the first time. All future accesses will + return the cached value as stored in the Instance's dict or slot. + """ + __slots__ = tuple() + + def __getattr__(self, attr): + """ + Whenever an attribute is requested that we do not know, we allow it + to be created and set. Next time the same attribute is reqeusted, it is simply + returned from our dict/slots. + """ + self._set_cache_(attr) + # will raise in case the cache was not created + return object.__getattribute__(self, attr) + + def _set_cache_(self, attr): + """ This method should be overridden in the derived class. + It should check whether the attribute named by attr can be created + and cached. Do nothing if you do not know the attribute or call your subclass + + The derived class may create as many additional attributes as it deems + necessary in case a git command returns more information than represented + in the single attribute.""" + pass + + +class Iterable(object): + """ + Defines an interface for iterable items which is to assure a uniform + way to retrieve and iterate items within the git repository + """ + __slots__ = tuple() + + @classmethod + def list_items(cls, repo, *args, **kwargs): + """ + Find all items of this type - subclasses can specify args and kwargs differently. + If no args are given, subclasses are obliged to return all items if no additional + arguments arg given. + + Note: Favor the iter_items method as it will + + Returns: + list(Item,...) list of item instances + """ + return list(cls.iter_items(repo, *args, **kwargs)) + + + @classmethod + def iter_items(cls, repo, *args, **kwargs): + """ + For more information about the arguments, see list_items + Return: + iterator yielding Items + """ + raise NotImplementedError("To be implemented by Subclass") + @@ -1,9 +1,9 @@ try: - from setuptools import setup, find_packages + from setuptools import setup, find_packages except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup, find_packages + 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 @@ -15,52 +15,52 @@ VERSION = v.readline().strip() v.close() class build_py(_build_py): - def run(self): - init = path.join(self.build_lib, 'git', '__init__.py') - if path.exists(init): - os.unlink(init) - _build_py.run(self) - _stamp_version(init) - self.byte_compile([init]) + def run(self): + init = path.join(self.build_lib, 'git', '__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', '__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 make_release_tree (self, base_dir, files): + _sdist.make_release_tree(self, base_dir, files) + orig = path.join('lib', 'git', '__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("'git'", "'%s'" % VERSION) - found = True - out.append(line) - f.close() + found, out = False, [] + f = open(filename, 'r') + for line in f: + if '__version__ =' in line: + line = line.replace("'git'", "'%s'" % VERSION) + found = True + out.append(line) + f.close() - if found: - f = open(filename, 'w') - f.writelines(out) - 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://gitorious.org/projects/git-python/", - packages = find_packages('lib'), - package_dir = {'':'lib'}, - license = "BSD License", - long_description = """\ + cmdclass={'build_py': build_py, 'sdist': sdist}, + version = VERSION, + description = "Python Git Library", + author = "Michael Trier", + author_email = "mtrier@gmail.com", + url = "http://gitorious.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 @@ -70,14 +70,14 @@ 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", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2.5", - "Programming Language :: Python :: 2.6", - "Topic :: Software Development :: Libraries :: Python Modules", - ] - ) + classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.5", + "Programming Language :: Python :: 2.6", + "Topic :: Software Development :: Libraries :: Python Modules", + ] + ) diff --git a/test/fixtures/diff_mode_only b/test/fixtures/diff_mode_only index 6fc18f69..6fc18f69 100644..100755 --- a/test/fixtures/diff_mode_only +++ b/test/fixtures/diff_mode_only diff --git a/test/fixtures/for_each_ref b/test/fixtures/for_each_ref Binary files differdeleted file mode 100644 index e56f5262..00000000 --- a/test/fixtures/for_each_ref +++ /dev/null diff --git a/test/fixtures/for_each_ref_tags b/test/fixtures/for_each_ref_tags Binary files differdeleted file mode 100644 index c4df85c6..00000000 --- a/test/fixtures/for_each_ref_tags +++ /dev/null diff --git a/test/fixtures/for_each_ref_with_path_component b/test/fixtures/for_each_ref_with_path_component Binary files differindex 717c4203..e723b4ae 100644 --- a/test/fixtures/for_each_ref_with_path_component +++ b/test/fixtures/for_each_ref_with_path_component diff --git a/test/git/test_actor.py b/test/git/test_actor.py index 862010fc..b7c2af7c 100644 --- a/test/git/test_actor.py +++ b/test/git/test_actor.py @@ -9,20 +9,20 @@ from test.testlib import * from git 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_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_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('<git.Actor "Michael Trier <mtrier@example.com>">', repr(a)) + def test_should_display_representation(self): + a = Actor._from_string("Michael Trier <mtrier@example.com>") + assert_equal('<git.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 + 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_base.py b/test/git/test_base.py new file mode 100644 index 00000000..402cdba3 --- /dev/null +++ b/test/git/test_base.py @@ -0,0 +1,93 @@ +# test_base.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +import time +from test.testlib import * +from git import * +import git.objects.base as base +import git.refs as refs +from itertools import chain +from git.objects.utils import get_object_type_by_name + +class TestBase(object): + + type_tuples = ( ("blob", "8741fc1d09d61f02ffd8cded15ff603eff1ec070"), + ("tree", "3a6a5e3eeed3723c09f1ef0399f81ed6b8d82e79"), + ("commit", "4251bd59fb8e11e40c40548cba38180a9536118c"), + ("tag", "e56a60e8e9cd333cfba0140a77cd12b0d9398f10") ) + + def setup(self): + self.repo = Repo(GIT_REPO) + + def test_base_object(self): + # test interface of base object classes + types = (Blob, Tree, Commit, TagObject) + assert len(types) == len(self.type_tuples) + + s = set() + num_objs = 0 + num_index_objs = 0 + for obj_type, (typename, hexsha) in zip(types, self.type_tuples): + item = obj_type(self.repo,hexsha) + num_objs += 1 + assert item.id == hexsha + assert item.type == typename + assert item.size + assert item.data + assert item == item + assert not item != item + assert str(item) == item.id + assert repr(item) + s.add(item) + + if isinstance(item, base.IndexObject): + num_index_objs += 1 + if hasattr(item,'path'): # never runs here + assert not item.path.startswith("/") # must be relative + assert isinstance(item.mode, int) + # END index object check + # END for each object type to create + + # each has a unique sha + assert len(s) == num_objs + assert len(s|s) == num_objs + assert num_index_objs == 2 + + + def test_tags(self): + # tag refs can point to tag objects or to commits + s = set() + ref_count = 0 + for ref in chain(self.repo.tags, self.repo.heads): + ref_count += 1 + assert isinstance(ref, refs.Ref) + assert str(ref) == ref.name + assert repr(ref) + assert ref == ref + assert not ref != ref + s.add(ref) + # END for each ref + assert len(s) == ref_count + assert len(s|s) == ref_count + + def test_heads(self): + # see how it dynmically updates its object + for head in self.repo.heads: + head.name + head.path + prev_object = head.object + cur_object = head.object + assert prev_object == cur_object # represent the same git object + assert prev_object is not cur_object # but are different instances + # END for each head + + def test_get_object_type_by_name(self): + for tname in base.Object.TYPES: + assert base.Object in get_object_type_by_name(tname).mro() + # END for each known type + + assert_raises( ValueError, get_object_type_by_name, "doesntexist" ) + diff --git a/test/git/test_blob.py b/test/git/test_blob.py index 5bd74ff7..266f3a23 100644 --- a/test/git/test_blob.py +++ b/test/git/test_blob.py @@ -9,89 +9,25 @@ from test.testlib import * from git import * class TestBlob(object): - def setup(self): - self.repo = Repo(GIT_REPO) - - @patch_object(Git, '_call_process') - 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, 'with_raw_output': True})) - - @patch_object(Git, '_call_process') - def test_should_return_blob_contents_with_newline(self, git): - git.return_value = fixture('cat_file_blob_nl') - blob = Blob(self.repo, **{'id': 'abc'}) - assert_equal("Hello world\n", blob.data) - assert_true(git.called) - assert_equal(git.call_args, (('cat_file', 'abc'), {'p': True, 'with_raw_output': True})) - - @patch_object(Git, '_call_process') - 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, 'with_raw_output': True})) - - @patch_object(Git, '_call_process') - 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, blob.size) - assert_true(git.called) - assert_equal(git.call_args, (('cat_file', 'abc'), {'s': True})) - - @patch_object(Git, '_call_process') - def test_should_cache_file_size(self, git): - git.return_value = fixture('cat_file_blob_size') - blob = Blob(self.repo, **{'id': 'abc'}) - assert_equal(11, blob.size) - assert_equal(11, blob.size) - assert_true(git.called) - assert_equal(git.call_count, 1) - 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) + def setup(self): + self.repo = Repo(GIT_REPO) + + def test_should_cache_data(self): + bid = 'a802c139d4767c89dcad79d836d05f7004d39aac' + blob = Blob(self.repo, bid) + blob.data + assert blob.data + blob.size + blob.size + + def test_mime_type_should_return_mime_type_for_known_types(self): + blob = Blob(self.repo, **{'id': 'abc', 'path': 'foo.png'}) + assert_equal("image/png", blob.mime_type) - @patch_object(Git, '_call_process') - 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( 2, len(b[0]) ) - # 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) - - # test the 'lines per commit' entries - tlist = b[0][1] - assert_true( tlist ) - assert_true( isinstance( tlist[0], basestring ) ) - assert_true( len( tlist ) < sum( len(t) for t in tlist ) ) # test for single-char bug - + def test_mime_type_should_return_text_plain_for_unknown_types(self): + blob = Blob(self.repo, **{'id': 'abc','path': 'something'}) + assert_equal("text/plain", blob.mime_type) - def test_should_return_appropriate_representation(self): - blob = Blob(self.repo, **{'id': 'abc'}) - assert_equal('<git.Blob "abc">', repr(blob)) + def test_should_return_appropriate_representation(self): + blob = Blob(self.repo, **{'id': 'abc'}) + assert_equal('<git.Blob "abc">', repr(blob)) diff --git a/test/git/test_commit.py b/test/git/test_commit.py index d6bd6120..a95fb675 100644 --- a/test/git/test_commit.py +++ b/test/git/test_commit.py @@ -8,241 +8,225 @@ from test.testlib import * from git import * class TestCommit(object): - def setup(self): - self.repo = Repo(GIT_REPO) + def setup(self): + self.repo = Repo(GIT_REPO) - @patch_object(Git, '_call_process') - def test_bake(self, git): - git.return_value = fixture('rev_list_single') + def test_bake(self): - commit = Commit(self.repo, **{'id': '4c8124ffcf4039d292442eeccabdeca5af5c5017'}) - commit.author # bake + commit = Commit(self.repo, **{'id': '2454ae89983a4496a445ce347d7a41c0bb0ea7ae'}) + commit.author # bake - assert_equal("Tom Preston-Werner", commit.author.name) - assert_equal("tom@mojombo.com", commit.author.email) + assert_equal("Sebastian Thiel", commit.author.name) + assert_equal("byronimo@gmail.com", commit.author.email) - assert_true(git.called) - assert_equal(git.call_args, (('rev_list', '4c8124ffcf4039d292442eeccabdeca5af5c5017', '--', ''), {'pretty': 'raw', 'max_count': 1})) - @patch_object(Git, '_call_process') - def test_id_abbrev(self, git): - git.return_value = fixture('rev_list_commit_idabbrev') - assert_equal('80f136f', self.repo.commit('80f136f500dfdb8c3e8abf4ae716f875f0a1b57f').id_abbrev) + @patch_object(Git, '_call_process') + def test_diff(self, git): + git.return_value = fixture('diff_p') - @patch_object(Git, '_call_process') - def test_diff(self, git): - git.return_value = fixture('diff_p') + diffs = Commit.diff(self.repo, 'master') - diffs = Commit.diff(self.repo, 'master') + assert_equal(15, len(diffs)) + + diff = diffs[0] + assert_equal('.gitignore', diff.a_blob.path) + assert_equal('.gitignore', diff.b_blob.path) + assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diff.a_blob.id) + assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diff.b_blob.id) + + assert_mode_644(diff.b_blob.mode) + + assert_equal(False, diff.new_file) + assert_equal(False, diff.deleted_file) + assert_equal("--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1,2 @@\n coverage\n+pkg", diff.diff) - assert_equal(15, len(diffs)) - - assert_equal('.gitignore', diffs[0].a_blob.name) - assert_equal('.gitignore', diffs[0].b_blob.name) - assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diffs[0].a_blob.id) - assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diffs[0].b_blob.id) - assert_equal('100644', diffs[0].b_blob.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].b_blob.name) - assert_equal(None, diffs[5].a_blob) - assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diffs[5].b_blob.id) - assert_equal( None, diffs[5].a_mode ) - assert_equal(True, diffs[5].new_file) - - assert_true(git.called) - assert_equal(git.call_args, (('diff', '-M', 'master'), {'full_index': True})) - - @patch_object(Git, '_call_process') - def test_diff_with_rename(self, git): - git.return_value = fixture('diff_rename') - - diffs = Commit.diff(self.repo, 'rename') - - assert_equal(1, len(diffs)) - - diff = diffs[0] - assert_true(diff.renamed) - assert_equal(diff.rename_from, 'AUTHORS') - assert_equal(diff.rename_to, 'CONTRIBUTORS') - - assert_true(git.called) - assert_equal(git.call_args, (('diff', '-M', 'rename'), {'full_index': True})) - - @patch_object(Git, '_call_process') - 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', '-M', '59ddc32', '13d27d5'), {'full_index': True})) - - @patch_object(Git, '_call_process') - 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_blob.name) - - assert_true(git.called) - assert_equal(git.call_args, (('diff', '-M', '59ddc32', '--', 'lib'), {'full_index': True})) - - @patch_object(Git, '_call_process') - 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_blob.name) - - assert_true(git.called) - assert_equal(git.call_args, (('diff', '-M', '59ddc32', '13d27d5', '--', 'lib'), {'full_index': True})) - - @patch_object(Git, '_call_process') - 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_blob.name) - assert_equal('.gitignore', diffs[0].b_blob.name) - assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diffs[0].a_blob.id) - assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diffs[0].b_blob.id) - assert_equal('100644', diffs[0].b_blob.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].b_blob.name) - assert_equal(None, diffs[5].a_blob) - assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diffs[5].b_blob.id) - assert_equal(True, diffs[5].new_file) - - assert_true(git.called) - assert_equal(git.call_args, (('diff', '-M', - '038af8c329ef7c1bae4568b98bd5c58510465493', - '91169e1f5fa4de2eaea3f176461f5dc784796769', - ), {'full_index': True})) - - @patch_object(Git, '_call_process') - 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].b_blob.name) - assert_equal(None, diffs[0].a_blob) - assert_equal('100644', diffs[0].b_blob.mode) - assert_equal('81d2c27608b352814cbe979a6acd678d30219678', diffs[0].b_blob.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].b_blob.name) - assert_equal(None, diffs[5].a_blob) - assert_equal('32cec87d1e78946a827ddf6a8776be4d81dcf1d1', diffs[5].b_blob.id) - assert_equal(True, diffs[5].new_file) - - assert_true(git.called) - assert_equal(git.call_args, (('show', '634396b2f541a9f2d58b00be1a07f0c358b999b3', '-M'), {'full_index': True, 'pretty': 'raw'})) - - @patch_object(Git, '_call_process') - 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', '-M'), {'full_index': True, 'pretty': 'raw'})) - - @patch_object(Git, '_call_process') - 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 - - # in case of mode-only changes, there is no blob - assert_equal(23, len(diffs)) - assert_equal(None, diffs[0].a_blob) - assert_equal(None, diffs[0].b_blob) - 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', '-M'), {'full_index': True, 'pretty': 'raw'})) - - @patch_object(Git, '_call_process') - def test_stats(self, git): - git.return_value = fixture('diff_tree_numstat_root') - - 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_tree', '634396b2f541a9f2d58b00be1a07f0c358b999b3', '--'), {'numstat': True, 'root': True })) - - @patch_object(Git, '_call_process') - def test_rev_list_bisect_all(self, git): - """ - 'git rev-list --bisect-all' returns additional information - in the commit header. This test ensures that we properly parse it. - """ - - git.return_value = fixture('rev_list_bisect_all') - - revs = self.repo.git.rev_list('HEAD', - pretty='raw', - first_parent=True, - bisect_all=True) - assert_true(git.called) - - commits = Commit.list_from_string(self.repo, revs) - expected_ids = ( - 'cf37099ea8d1d8c7fbf9b6d12d7ec0249d3acb8b', - '33ebe7acec14b25c5f84f35a664803fcab2f7781', - 'a6604a00a652e754cb8b6b0b9f194f839fc38d7c', - '8df638c22c75ddc9a43ecdde90c0c9939f5009e7', - 'c231551328faa864848bde6ff8127f59c9566e90', - ) - for sha1, commit in zip(expected_ids, commits): - assert_equal(sha1, commit.id) - - 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('<git.Commit "abc">', repr(commit)) - - def test_equality(self): - commit1 = Commit(self.repo, id='abc') - commit2 = Commit(self.repo, id='abc') - commit3 = Commit(self.repo, id='zyx') - assert_equal(commit1, commit2) - assert_not_equal(commit2, commit3) + diff = diffs[5] + assert_equal('lib/grit/actor.rb', diff.b_blob.path) + assert_equal(None, diff.a_blob) + assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diff.b_blob.id) + assert_equal( None, diff.a_mode ) + assert_equal(True, diff.new_file) + + assert_true(git.called) + assert_equal(git.call_args, (('diff', '-M', 'master'), {'full_index': True})) + + @patch_object(Git, '_call_process') + def test_diff_with_rename(self, git): + git.return_value = fixture('diff_rename') + + diffs = Commit.diff(self.repo, 'rename') + + assert_equal(1, len(diffs)) + + diff = diffs[0] + assert_true(diff.renamed) + assert_equal(diff.rename_from, 'AUTHORS') + assert_equal(diff.rename_to, 'CONTRIBUTORS') + + assert_true(git.called) + assert_equal(git.call_args, (('diff', '-M', 'rename'), {'full_index': True})) + + @patch_object(Git, '_call_process') + 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', '-M', '59ddc32', '13d27d5'), {'full_index': True})) + + @patch_object(Git, '_call_process') + 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_blob.path) + + assert_true(git.called) + assert_equal(git.call_args, (('diff', '-M', '59ddc32', '--', 'lib'), {'full_index': True})) + + @patch_object(Git, '_call_process') + 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_blob.path) + + assert_true(git.called) + assert_equal(git.call_args, (('diff', '-M', '59ddc32', '13d27d5', '--', 'lib'), {'full_index': True})) + + @patch_object(Git, '_call_process') + 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)) + + diff = diffs[0] + assert_equal('.gitignore', diff.a_blob.path) + assert_equal('.gitignore', diff.b_blob.path) + assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diff.a_blob.id) + assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diff.b_blob.id) + assert_mode_644(diff.b_blob.mode) + assert_equal(False, diff.new_file) + assert_equal(False, diff.deleted_file) + assert_equal("--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1,2 @@\n coverage\n+pkg", diff.diff) + + diff = diffs[5] + assert_equal('lib/grit/actor.rb', diff.b_blob.path) + assert_equal(None, diff.a_blob) + assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diff.b_blob.id) + assert_equal(True, diff.new_file) + + assert_true(git.called) + assert_equal(git.call_args, (('diff', '-M', + '038af8c329ef7c1bae4568b98bd5c58510465493', + '91169e1f5fa4de2eaea3f176461f5dc784796769', + ), {'full_index': True})) + + def test_diffs_on_initial_import(self): + commit = Commit(self.repo, '33ebe7acec14b25c5f84f35a664803fcab2f7781') + + for diff in commit.diffs: + assert isinstance(diff, Diff) + assert isinstance(diff.a_blob, Blob) or isinstance(diff.b_blob, Blob) + + if diff.a_mode is not None: + assert isinstance(diff.a_mode, int) + if diff.b_mode is not None: + isinstance(diff.b_mode, int) + + assert diff.diff is not None # can be empty + + if diff.renamed: + assert diff.rename_from and diff.rename_to and diff.rename_from != diff.rename_to + if diff.a_blob is None: + assert diff.new_file and isinstance(diff.new_file, bool) + if diff.b_blob is None: + assert diff.deleted_file and isinstance(diff.deleted_file, bool) + # END for each diff in initial import commit + + def test_diffs_on_initial_import_without_parents(self): + commit = Commit(self.repo, id='33ebe7acec14b25c5f84f35a664803fcab2f7781') + diffs = commit.diffs + assert diffs + + def test_diffs_with_mode_only_change(self): + commit = Commit(self.repo, id='ccde80b7a3037a004a7807a6b79916ce2a1e9729') + diffs = commit.diffs + + # in case of mode-only changes, there is no blob + assert_equal(1, len(diffs)) + assert_equal(None, diffs[0].a_blob) + assert_equal(None, diffs[0].b_blob) + assert_mode_644(diffs[0].a_mode) + assert_mode_755(diffs[0].b_mode) + + def test_stats(self): + commit = Commit(self.repo, id='33ebe7acec14b25c5f84f35a664803fcab2f7781') + stats = commit.stats + + def check_entries(d): + assert isinstance(d, dict) + for key in ("insertions", "deletions", "lines"): + assert key in d + # END assertion helper + assert stats.files + assert stats.total + + check_entries(stats.total) + assert "files" in stats.total + + for filepath, d in stats.files.items(): + check_entries(d) + # END for each stated file + + @patch_object(Git, '_call_process') + def test_rev_list_bisect_all(self, git): + """ + 'git rev-list --bisect-all' returns additional information + in the commit header. This test ensures that we properly parse it. + """ + + git.return_value = fixture('rev_list_bisect_all') + + revs = self.repo.git.rev_list('HEAD', + pretty='raw', + first_parent=True, + bisect_all=True) + assert_true(git.called) + + commits = Commit._iter_from_process_or_stream(self.repo, ListProcessAdapter(revs)) + expected_ids = ( + 'cf37099ea8d1d8c7fbf9b6d12d7ec0249d3acb8b', + '33ebe7acec14b25c5f84f35a664803fcab2f7781', + 'a6604a00a652e754cb8b6b0b9f194f839fc38d7c', + '8df638c22c75ddc9a43ecdde90c0c9939f5009e7', + 'c231551328faa864848bde6ff8127f59c9566e90', + ) + for sha1, commit in zip(expected_ids, commits): + assert_equal(sha1, commit.id) + + 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('<git.Commit "abc">', repr(commit)) + + def test_equality(self): + commit1 = Commit(self.repo, id='abc') + commit2 = Commit(self.repo, id='abc') + commit3 = Commit(self.repo, id='zyx') + assert_equal(commit1, commit2) + assert_not_equal(commit2, commit3) + diff --git a/test/git/test_diff.py b/test/git/test_diff.py index 65a27e98..b2339455 100644 --- a/test/git/test_diff.py +++ b/test/git/test_diff.py @@ -8,23 +8,23 @@ from test.testlib import * from git import * class TestDiff(object): - def setup(self): - self.repo = Repo(GIT_REPO) + 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())) + 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())) - def test_diff_with_rename(self): - output = fixture('diff_rename') - diffs = Diff.list_from_string(self.repo, output) + def test_diff_with_rename(self): + output = fixture('diff_rename') + diffs = Diff._list_from_string(self.repo, output) - assert_equal(1, len(diffs)) + assert_equal(1, len(diffs)) - diff = diffs[0] - assert_true(diff.renamed) - assert_equal(diff.rename_from, 'AUTHORS') - assert_equal(diff.rename_to, 'CONTRIBUTORS') + diff = diffs[0] + assert_true(diff.renamed) + assert_equal(diff.rename_from, 'AUTHORS') + assert_equal(diff.rename_to, 'CONTRIBUTORS') diff --git a/test/git/test_git.py b/test/git/test_git.py index 6e28f4c0..1f44aebc 100644 --- a/test/git/test_git.py +++ b/test/git/test_git.py @@ -9,50 +9,81 @@ from test.testlib import * from git import Git, GitCommandError class TestGit(object): - def setup(self): - base = os.path.join(os.path.dirname(__file__), "../..") - self.git = Git(base) - - @patch_object(Git, 'execute') - def test_call_process_calls_execute(self, git): - git.return_value = '' - self.git.version() - assert_true(git.called) - assert_equal(git.call_args, ((['git', 'version'],), {})) - - @raises(GitCommandError) - def test_it_raises_errors(self): - self.git.this_does_not_exist() - - - def test_it_transforms_kwargs_into_git_command_arguments(self): - assert_equal(["-s"], self.git.transform_kwargs(**{'s': True})) - assert_equal(["-s5"], 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\.]{2}.*$', self.git.execute(["git","version"])) - - def test_it_accepts_stdin(self): - filename = fixture_path("cat_file_blob") - fh = open(filename, 'r') - assert_equal("70c379b63ffa0795fdbfbc128e5a2818397b7ef8", - self.git.hash_object(istream=fh, stdin=True)) - fh.close() - - def test_it_handles_large_input(self): - if sys.platform == 'win32': - output = self.git.execute(["type", "C:\WINDOWS\system32\cmd.exe"]) - else: - output = self.git.execute(["cat", "/bin/bash"]) - assert_true(len(output) > 4096) # at least 4k - - @patch_object(Git, 'execute') - def test_it_ignores_false_kwargs(self, git): - # this_should_not_be_ignored=False implies it *should* be ignored - output = self.git.version(pass_this_kwarg=False) - assert_true("pass_this_kwarg" not in git.call_args[1]) + def setup(self): + self.git = Git(GIT_REPO) + + @patch_object(Git, 'execute') + def test_call_process_calls_execute(self, git): + git.return_value = '' + self.git.version() + assert_true(git.called) + assert_equal(git.call_args, ((['git', 'version'],), {})) + + @raises(GitCommandError) + def test_it_raises_errors(self): + self.git.this_does_not_exist() + + + def test_it_transforms_kwargs_into_git_command_arguments(self): + assert_equal(["-s"], self.git.transform_kwargs(**{'s': True})) + assert_equal(["-s5"], 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\.]{2}.*$', self.git.execute(["git","version"])) + + def test_it_accepts_stdin(self): + filename = fixture_path("cat_file_blob") + fh = open(filename, 'r') + assert_equal("70c379b63ffa0795fdbfbc128e5a2818397b7ef8", + self.git.hash_object(istream=fh, stdin=True)) + fh.close() + + def test_it_handles_large_input(self): + if sys.platform == 'win32': + output = self.git.execute(["type", "C:\WINDOWS\system32\cmd.exe"]) + else: + output = self.git.execute(["cat", "/bin/bash"]) + assert_true(len(output) > 4096) # at least 4k + + @patch_object(Git, 'execute') + def test_it_ignores_false_kwargs(self, git): + # this_should_not_be_ignored=False implies it *should* be ignored + output = self.git.version(pass_this_kwarg=False) + assert_true("pass_this_kwarg" not in git.call_args[1]) + + def test_persistent_cat_file_command(self): + # read header only + import subprocess as sp + hexsha = "b2339455342180c7cc1e9bba3e9f181f7baa5167" + g = self.git.cat_file(batch_check=True, istream=sp.PIPE,as_process=True) + g.stdin.write("b2339455342180c7cc1e9bba3e9f181f7baa5167\n") + g.stdin.flush() + obj_info = g.stdout.readline() + + # read header + data + g = self.git.cat_file(batch=True, istream=sp.PIPE,as_process=True) + g.stdin.write("b2339455342180c7cc1e9bba3e9f181f7baa5167\n") + g.stdin.flush() + obj_info_two = g.stdout.readline() + assert obj_info == obj_info_two + + # read data - have to read it in one large chunk + size = int(obj_info.split()[2]) + data = g.stdout.read(size) + terminating_newline = g.stdout.read(1) + + # now we should be able to read a new object + g.stdin.write("b2339455342180c7cc1e9bba3e9f181f7baa5167\n") + g.stdin.flush() + assert g.stdout.readline() == obj_info + + + # same can be achived using the respective command functions + hexsha, typename, size = self.git.get_object_header(hexsha) + hexsha, typename_two, size_two, data = self.git.get_object_data(hexsha) + assert typename == typename_two and size == size_two diff --git a/test/git/test_head.py b/test/git/test_head.py index e3408974..b8380838 100644 --- a/test/git/test_head.py +++ b/test/git/test_head.py @@ -8,25 +8,19 @@ from test.testlib import * from git import * class TestHead(object): - def setup(self): - self.repo = Repo(GIT_REPO) + def setup(self): + self.repo = Repo(GIT_REPO) - @patch_object(Git, '_call_process') - def test_repr(self, git): - git.return_value = fixture('for_each_ref') - - head = self.repo.heads[0] - - assert_equal('<git.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)'})) + def test_base(self): + for head in self.repo.heads: + assert head.name + assert "refs/heads" in head.path + # END for each head - @patch_object(Git, '_call_process') - def test_ref_with_path_component(self, git): - git.return_value = fixture('for_each_ref_with_path_component') - head = self.repo.heads[0] + @patch_object(Git, '_call_process') + def test_ref_with_path_component(self, git): + git.return_value = fixture('for_each_ref_with_path_component') + head = self.repo.heads[0] - assert_equal('refactoring/feature1', head.name) - assert_true(git.called) - assert_equal(git.call_args, (('for_each_ref', 'refs/heads'), {'sort': 'committerdate', 'format': '%(refname)%00%(objectname)'})) + assert_equal('refactoring/feature1', head.name) + assert_true(git.called) diff --git a/test/git/test_performance.py b/test/git/test_performance.py new file mode 100644 index 00000000..96f13a2e --- /dev/null +++ b/test/git/test_performance.py @@ -0,0 +1,38 @@ +# test_performance.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +from test.testlib import * +from git import * +from time import time + +class TestPerformance(object): + def setup(self): + self.repo = Repo(GIT_REPO) + + def test_iteration(self): + num_objs = 0 + num_commits = 0 + + # find the first commit containing the given path - always do a full + # iteration ( restricted to the path in question ), but in fact it should + # return quite a lot of commits, we just take one and hence abort the operation + + st = time() + for c in self.repo.commits(): + num_commits += 1 + c.author + c.authored_date + c.committer + c.committed_date + c.message + for obj in c.tree.traverse(): + obj.size + num_objs += 1 + # END for each object + # END for each commit + elapsed_time = time() - st + print "Traversed %i Trees and a total of %i unchached objects in %s [s] ( %f objs/s )" % (num_commits, num_objs, elapsed_time, num_objs/elapsed_time) + diff --git a/test/git/test_repo.py b/test/git/test_repo.py index a9d3beaf..e998ac6d 100644 --- a/test/git/test_repo.py +++ b/test/git/test_repo.py @@ -10,255 +10,237 @@ from test.testlib import * from git import * class TestRepo(object): - def setup(self): - self.repo = Repo(GIT_REPO) - - @raises(InvalidGitRepositoryError) - def test_new_should_raise_on_invalid_repo_location(self): - if sys.platform == "win32": - Repo("C:\\WINDOWS\\Temp") - else: - Repo("/tmp") + def setup(self): + self.repo = Repo(GIT_REPO) + + @raises(InvalidGitRepositoryError) + def test_new_should_raise_on_invalid_repo_location(self): + if sys.platform == "win32": + Repo("C:\\WINDOWS\\Temp") + else: + Repo("/tmp") + + @raises(NoSuchPathError) + def test_new_should_raise_on_non_existant_path(self): + Repo("repos/foobar") + + def test_description(self): + txt = "Test repository" + self.repo.description = txt + assert_equal(self.repo.description, txt) + + def test_heads_should_return_array_of_head_objects(self): + for head in self.repo.heads: + assert_equal(Head, head.__class__) + + def test_heads_should_populate_head_data(self): + for head in self.repo.heads: + assert head.name + assert isinstance(head.commit,Commit) + # END for each head + + @patch_object(Git, '_call_process') + def test_commits(self, git): + git.return_value = ListProcessAdapter(fixture('rev_list')) + + commits = self.repo.commits('master', max_count=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(tuple(), c.parents) + + c = commits[2] + assert_equal(["6e64c55896aabb9a7d8e9f8f296f426d21a78c2c", "7f874954efb9ba35210445be456c74e037ba6af2"], map(lambda p: p.id, c.parents)) + assert_equal("Merge branch 'site'", c.summary) + + assert_true(git.called) + + @patch_object(Git, '_call_process') + def test_commit_count(self, git): + 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', '--', ''), {})) + + @patch_object(Git, '_call_process') + def test_commit(self, git): + git.return_value = ListProcessAdapter(fixture('rev_list_single')) + + commit = self.repo.commit('4c8124ffcf4039d292442eeccabdeca5af5c5017') + + assert_equal("4c8124ffcf4039d292442eeccabdeca5af5c5017", commit.id) + + assert_true(git.called) + + @patch_object(Repo, '__init__') + @patch_object(Git, '_call_process') + def test_init_bare(self, git, repo): + git.return_value = True + repo.return_value = None + + Repo.init_bare("repos/foo/bar.git") + + assert_true(git.called) + assert_equal(git.call_args, (('init', '--bare'), {})) + assert_true(repo.called) + assert_equal(repo.call_args, (('repos/foo/bar.git',), {})) + + @patch_object(Repo, '__init__') + @patch_object(Git, '_call_process') + def test_init_bare_with_options(self, git, repo): + git.return_value = True + repo.return_value = None + + Repo.init_bare("repos/foo/bar.git", **{'template': "/baz/sweet"}) + + assert_true(git.called) + assert_equal(git.call_args, (('init', '--bare'), {'template': '/baz/sweet'})) + assert_true(repo.called) + assert_equal(repo.call_args, (('repos/foo/bar.git',), {})) + + @patch_object(Repo, '__init__') + @patch_object(Git, '_call_process') + def test_fork_bare(self, git, repo): + git.return_value = None + repo.return_value = None - @raises(NoSuchPathError) - def test_new_should_raise_on_non_existant_path(self): - Repo("repos/foobar") + self.repo.fork_bare("repos/foo/bar.git") - def test_description(self): - txt = "Test repository" - self.repo.description = txt - assert_equal(self.repo.description, txt) + assert_true(git.called) + path = os.path.join(absolute_project_path(), '.git') + assert_equal(git.call_args, (('clone', path, 'repos/foo/bar.git'), {'bare': True})) + assert_true(repo.called) - def test_heads_should_return_array_of_head_objects(self): - for head in self.repo.heads: - assert_equal(Head, head.__class__) + @patch_object(Repo, '__init__') + @patch_object(Git, '_call_process') + def test_fork_bare_with_options(self, git, repo): + git.return_value = None + repo.return_value = None - @patch_object(Git, '_call_process') - def test_heads_should_populate_head_data(self, git): - git.return_value = fixture('for_each_ref') + self.repo.fork_bare("repos/foo/bar.git", **{'template': '/awesome'}) - head = self.repo.heads[0] - assert_equal('master', head.name) - assert_equal('634396b2f541a9f2d58b00be1a07f0c358b999b3', head.commit.id) + assert_true(git.called) + path = os.path.join(absolute_project_path(), '.git') + assert_equal(git.call_args, (('clone', path, 'repos/foo/bar.git'), + {'bare': True, 'template': '/awesome'})) + assert_true(repo.called) - assert_true(git.called) - assert_equal(git.call_args, (('for_each_ref', 'refs/heads'), {'sort': 'committerdate', 'format': '%(refname)%00%(objectname)'})) + @patch_object(Git, '_call_process') + def test_diff(self, git): + self.repo.diff('master^', 'master') - @patch_object(Git, '_call_process') - def test_commits(self, git): - git.return_value = fixture('rev_list') + assert_true(git.called) + assert_equal(git.call_args, (('diff', 'master^', 'master', '--'), {})) - commits = self.repo.commits('master', max_count=10) + self.repo.diff('master^', 'master', 'foo/bar') - 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) + assert_true(git.called) + assert_equal(git.call_args, (('diff', 'master^', 'master', '--', 'foo/bar'), {})) - c = commits[1] - assert_equal([], c.parents) + self.repo.diff('master^', 'master', 'foo/bar', 'foo/baz') - c = commits[2] - assert_equal(["6e64c55896aabb9a7d8e9f8f296f426d21a78c2c", "7f874954efb9ba35210445be456c74e037ba6af2"], map(lambda p: p.id, c.parents)) - assert_equal("Merge branch 'site'", c.summary) + assert_true(git.called) + assert_equal(git.call_args, (('diff', 'master^', 'master', '--', 'foo/bar', 'foo/baz'), {})) - assert_true(git.called) - assert_equal(git.call_args, (('rev_list', 'master', '--', ''), {'skip': 0, 'pretty': 'raw', 'max_count': 10})) + @patch_object(Git, '_call_process') + def test_diff_with_parents(self, git): + git.return_value = fixture('diff_p') - @patch_object(Git, '_call_process') - def test_commit_count(self, git): - git.return_value = fixture('rev_list_count') + diffs = self.repo.commit_diff('master') + assert_equal(15, len(diffs)) + assert_true(git.called) - assert_equal(655, self.repo.commit_count('master')) + def test_archive_tar(self): + assert self.repo.archive_tar() - assert_true(git.called) - assert_equal(git.call_args, (('rev_list', 'master', '--', ''), {})) + def test_archive_tar_gz(self): + assert self.repo.archive_tar_gz() - @patch_object(Git, '_call_process') - def test_commit(self, git): - git.return_value = fixture('rev_list_single') + @patch('git.utils.touch') + def test_enable_daemon_serve(self, touch): + self.repo.daemon_serve = False + assert_false(self.repo.daemon_serve) - commit = self.repo.commit('4c8124ffcf4039d292442eeccabdeca5af5c5017') - - assert_equal("4c8124ffcf4039d292442eeccabdeca5af5c5017", commit.id) - - assert_true(git.called) - assert_equal(git.call_args, (('rev_list', '4c8124ffcf4039d292442eeccabdeca5af5c5017', '--', ''), {'pretty': 'raw', 'max_count': 1})) - - @patch_object(Git, '_call_process') - def test_tree(self, git): - git.return_value = fixture('ls_tree_a') - - tree = self.repo.tree('master') - - assert_equal(4, len([c for c in tree.values() if isinstance(c, Blob)])) - assert_equal(3, len([c for c in tree.values() if isinstance(c, Tree)])) - - assert_true(git.called) - assert_equal(git.call_args, (('ls_tree', 'master'), {})) - - @patch_object(Git, '_call_process') - def test_blob(self, git): - 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, 'with_raw_output': True})) - - @patch_object(Repo, '__init__') - @patch_object(Git, '_call_process') - def test_init_bare(self, git, repo): - git.return_value = True - repo.return_value = None - - Repo.init_bare("repos/foo/bar.git") - - assert_true(git.called) - assert_equal(git.call_args, (('init', '--bare'), {})) - assert_true(repo.called) - assert_equal(repo.call_args, (('repos/foo/bar.git',), {})) - - @patch_object(Repo, '__init__') - @patch_object(Git, '_call_process') - def test_init_bare_with_options(self, git, repo): - git.return_value = True - repo.return_value = None - - Repo.init_bare("repos/foo/bar.git", **{'template': "/baz/sweet"}) - - assert_true(git.called) - assert_equal(git.call_args, (('init', '--bare'), {'template': '/baz/sweet'})) - assert_true(repo.called) - assert_equal(repo.call_args, (('repos/foo/bar.git',), {})) - - @patch_object(Repo, '__init__') - @patch_object(Git, '_call_process') - def test_fork_bare(self, git, repo): - git.return_value = None - repo.return_value = None - - self.repo.fork_bare("repos/foo/bar.git") - - assert_true(git.called) - path = os.path.join(absolute_project_path(), '.git') - assert_equal(git.call_args, (('clone', path, 'repos/foo/bar.git'), {'bare': True})) - assert_true(repo.called) - - @patch_object(Repo, '__init__') - @patch_object(Git, '_call_process') - def test_fork_bare_with_options(self, git, repo): - git.return_value = None - repo.return_value = None - - self.repo.fork_bare("repos/foo/bar.git", **{'template': '/awesome'}) - - assert_true(git.called) - path = os.path.join(absolute_project_path(), '.git') - assert_equal(git.call_args, (('clone', path, 'repos/foo/bar.git'), - {'bare': True, 'template': '/awesome'})) - assert_true(repo.called) - - @patch_object(Git, '_call_process') - def test_diff(self, git): - self.repo.diff('master^', 'master') - - assert_true(git.called) - assert_equal(git.call_args, (('diff', 'master^', 'master', '--'), {})) - - self.repo.diff('master^', 'master', 'foo/bar') - - assert_true(git.called) - assert_equal(git.call_args, (('diff', 'master^', 'master', '--', 'foo/bar'), {})) - - 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_object(Git, '_call_process') - def test_diff_with_parents(self, git): - 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): - assert self.repo.archive_tar() - - def test_archive_tar_gz(self): - assert self.repo.archive_tar_gz() - - @patch('git.utils.touch') - def test_enable_daemon_serve(self, touch): - self.repo.daemon_serve = False - assert_false(self.repo.daemon_serve) - - def test_disable_daemon_serve(self): - self.repo.daemon_serve = True - assert_true(self.repo.daemon_serve) + def test_disable_daemon_serve(self): + self.repo.daemon_serve = True + assert_true(self.repo.daemon_serve) - @patch_object(os.path, 'exists') - def test_alternates_no_file(self, os): - os.return_value = False - assert_equal([], self.repo.alternates) - - assert_true(os.called) - - @patch_object(os, 'remove') - def test_alternates_setter_empty(self, os): - self.repo.alternates = [] - assert_true(os.called) - - def test_repr(self): - path = os.path.join(os.path.abspath(GIT_REPO), '.git') - assert_equal('<git.Repo "%s">' % path, repr(self.repo)) - - @patch_object(Git, '_call_process') - 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_object(Git, '_call_process') - 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})) - - def test_is_dirty_with_bare_repository(self): - self.repo.bare = True - assert_false(self.repo.is_dirty) - - @patch_object(Git, '_call_process') - def test_is_dirty_with_clean_working_dir(self, git): - self.repo.bare = False - git.return_value = '' - assert_false(self.repo.is_dirty) - assert_equal(git.call_args, (('diff', 'HEAD', '--'), {})) - - @patch_object(Git, '_call_process') - def test_is_dirty_with_dirty_working_dir(self, git): - self.repo.bare = False - git.return_value = '''-aaa\n+bbb''' - assert_true(self.repo.is_dirty) - assert_equal(git.call_args, (('diff', 'HEAD', '--'), {})) - - @patch_object(Git, '_call_process') - def test_active_branch(self, git): - git.return_value = 'refs/heads/major-refactoring' - assert_equal(self.repo.active_branch, 'major-refactoring') - assert_equal(git.call_args, (('symbolic_ref', 'HEAD'), {})) + @patch_object(os.path, 'exists') + def test_alternates_no_file(self, os): + os.return_value = False + assert_equal([], self.repo.alternates) + + assert_true(os.called) + + @patch_object(os, 'remove') + def test_alternates_setter_empty(self, os): + self.repo.alternates = [] + assert_true(os.called) + + def test_repr(self): + path = os.path.join(os.path.abspath(GIT_REPO), '.git') + assert_equal('<git.Repo "%s">' % path, repr(self.repo)) + + def test_is_dirty_with_bare_repository(self): + self.repo.bare = True + assert_false(self.repo.is_dirty) + + @patch_object(Git, '_call_process') + def test_is_dirty_with_clean_working_dir(self, git): + self.repo.bare = False + git.return_value = '' + assert_false(self.repo.is_dirty) + assert_equal(git.call_args, (('diff', 'HEAD', '--'), {})) + + @patch_object(Git, '_call_process') + def test_is_dirty_with_dirty_working_dir(self, git): + self.repo.bare = False + git.return_value = '''-aaa\n+bbb''' + assert_true(self.repo.is_dirty) + assert_equal(git.call_args, (('diff', 'HEAD', '--'), {})) + + @patch_object(Git, '_call_process') + def test_active_branch(self, git): + git.return_value = 'refs/heads/major-refactoring' + assert_equal(self.repo.active_branch.name, 'major-refactoring') + assert_equal(git.call_args, (('symbolic_ref', 'HEAD'), {})) + + @patch_object(Git, '_call_process') + def test_should_display_blame_information(self, git): + git.return_value = fixture('blame') + b = self.repo.blame( 'master', 'lib/git.py') + assert_equal(13, len(b)) + assert_equal( 2, len(b[0]) ) + # 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) + + # test the 'lines per commit' entries + tlist = b[0][1] + assert_true( tlist ) + assert_true( isinstance( tlist[0], basestring ) ) + assert_true( len( tlist ) < sum( len(t) for t in tlist ) ) # test for single-char bug diff --git a/test/git/test_stats.py b/test/git/test_stats.py index b6f1b60e..706f29a4 100644 --- a/test/git/test_stats.py +++ b/test/git/test_stats.py @@ -8,20 +8,20 @@ from test.testlib import * from git 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']) + 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 index 732bbd45..2ebb860a 100644 --- a/test/git/test_tag.py +++ b/test/git/test_tag.py @@ -7,30 +7,29 @@ from mock import * from test.testlib import * from git import * +from git.objects.tag import TagObject +import time class TestTag(object): - def setup(self): - self.repo = Repo(GIT_REPO) + def setup(self): + self.repo = Repo(GIT_REPO) - @patch_object(Git, '_call_process') - 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)'})) + def test_tag_base(self): + tag_object_refs = list() + for tag in self.repo.tags: + assert "refs/tags" in tag.path + assert tag.name + assert isinstance( tag.commit, Commit ) + if tag.tag is not None: + tag_object_refs.append( tag ) + tagobj = tag.tag + assert isinstance( tagobj, TagObject ) + assert tagobj.tag == tag.name + assert isinstance( tagobj.tagger, Actor ) + assert isinstance( tagobj.tagged_date, time.struct_time ) + assert tagobj.message + # END if we have a tag object + # END for tag in repo-tags + assert tag_object_refs + - @patch_object(Git, '_call_process') - def test_repr(self, git): - git.return_value = fixture('for_each_ref') - - tag = self.repo.tags[0] - assert_equal('<git.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 index 947b0ffb..dafb6f3f 100644 --- a/test/git/test_tree.py +++ b/test/git/test_tree.py @@ -7,143 +7,38 @@ from test.testlib import * from git import * -class TestTree(object): - def setup(self): - self.repo = Repo(GIT_REPO) - - @patch_object(Git, '_call_process') - 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['grit'] - child.items() - child.items() - - 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 = 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) +class TestTree(TestCase): + + def setUp(self): + self.repo = Repo(GIT_REPO) + + + + def test_traverse(self): + root = self.repo.tree() + num_recursive = 0 + all_items = list() + for obj in root.traverse(): + if "/" in obj.path: + num_recursive += 1 + + assert isinstance(obj, (Blob, Tree)) + all_items.append(obj) + # END for each object + # limit recursion level to 0 - should be same as default iteration + assert all_items + assert 'CHANGES' in root + assert len(list(root)) == len(list(root.traverse(max_depth=0))) + + # only choose trees + trees_only = lambda i: i.type == "tree" + trees = list(root.traverse(predicate = trees_only)) + assert len(trees) == len(list( i for i in root.traverse() if trees_only(i) )) + + # trees and blobs + assert len(set(trees)|set(root.trees)) == len(trees) + assert len(set(b for b in root if isinstance(b, Blob)) | set(root.blobs)) == len( root.blobs ) - def test_content_from_string_tree_should_return_blob(self): - text = fixture('ls_tree_b').split("\n")[0] - - tree = 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 = Tree.content_from_string(None, text) - assert_none(tree) - - @raises(TypeError) - def test_content_from_string_invalid_type_should_raise(self): - Tree.content_from_string(None, "040000 bogus 650fa3f0c17f1edb4ae53d8dcca4ac59d86e6c44 test") - - @patch_object(Blob, 'size') - @patch_object(Git, '_call_process') - def test_slash(self, git, blob): - 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_object(Blob, 'size') - @patch_object(Git, '_call_process') - def test_slash_with_zero_length_file(self, git, blob): - git.return_value = fixture('ls_tree_a') - blob.return_value = 0 - - tree = self.repo.tree('master') - - assert_not_none(tree/'README.txt') - assert_equal('8b1e02c0fb554eed2ce2ef737a68bb369d7527df', (tree/'README.txt').id) - - assert_true(git.called) - assert_equal(git.call_args, (('ls_tree', 'master'), {})) - - @patch_object(Git, '_call_process') - 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'), {})) - - @patch_object(Blob, 'size') - @patch_object(Git, '_call_process') - def test_dict(self, git, blob): - 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_object(Blob, 'size') - @patch_object(Git, '_call_process') - def test_dict_with_zero_length_file(self, git, blob): - git.return_value = fixture('ls_tree_a') - blob.return_value = 0 - - tree = self.repo.tree('master') - - assert_not_none(tree['README.txt']) - assert_equal('8b1e02c0fb554eed2ce2ef737a68bb369d7527df', tree['README.txt'].id) - - assert_true(git.called) - assert_equal(git.call_args, (('ls_tree', 'master'), {})) - - @patch_object(Git, '_call_process') - def test_dict_with_commits(self, git): - git.return_value = fixture('ls_tree_commit') - - tree = self.repo.tree('master') - - assert_none(tree.get('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'), {})) - - @patch_object(Git, '_call_process') - @raises(KeyError) - def test_dict_with_non_existant_file(self, git): - git.return_value = fixture('ls_tree_commit') - - tree = self.repo.tree('master') - tree['bar'] - - def test_repr(self): - tree = Tree(self.repo, id='abc') - assert_equal('<git.Tree "abc">', repr(tree)) + def test_repr(self): + tree = Tree(self.repo, id='abc') + assert_equal('<git.Tree "abc">', repr(tree)) diff --git a/test/git/test_utils.py b/test/git/test_utils.py index 327a07ed..2983a14a 100644 --- a/test/git/test_utils.py +++ b/test/git/test_utils.py @@ -9,13 +9,13 @@ from test.testlib import * from git import * class TestUtils(object): - def setup(self): - self.testdict = { - "string": "42", - "int": 42, - "array": [ 42 ], - } + def setup(self): + self.testdict = { + "string": "42", + "int": 42, + "array": [ 42 ], + } - def test_it_should_dashify(self): - assert_equal('this-is-my-argument', dashify('this_is_my_argument')) - assert_equal('foo', dashify('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/testlib/__init__.py b/test/testlib/__init__.py index 77512794..f364171b 100644 --- a/test/testlib/__init__.py +++ b/test/testlib/__init__.py @@ -8,6 +8,7 @@ import inspect from mock import * from asserts import * from helper import * +from unittest import TestCase __all__ = [ name for name, obj in locals().items() - if not (name.startswith('_') or inspect.ismodule(obj)) ] + if not (name.startswith('_') or inspect.ismodule(obj)) ] diff --git a/test/testlib/asserts.py b/test/testlib/asserts.py index f66af122..46fcf20e 100644 --- a/test/testlib/asserts.py +++ b/test/testlib/asserts.py @@ -8,31 +8,43 @@ import re import unittest from nose import tools from nose.tools import * +import stat __all__ = ['assert_instance_of', 'assert_not_instance_of', - 'assert_none', 'assert_not_none', - 'assert_match', 'assert_not_match'] + tools.__all__ + 'assert_none', 'assert_not_none', + 'assert_match', 'assert_not_match', 'assert_mode_644', + 'assert_mode_755'] + tools.__all__ def assert_instance_of(expected, actual, msg=None): - """Verify that object is an instance of expected """ - assert isinstance(actual, expected), msg + """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) - + """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) + """verify that item is None""" + assert actual is None, msg def assert_not_none(actual, msg=None): - """verify that item is None""" - assert_not_equal(None, actual, msg) + """verify that item is None""" + assert actual is not None, msg def assert_match(pattern, string, msg=None): - """verify that the pattern matches the string""" - assert_not_none(re.search(pattern, string), msg) + """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 + """verify that the pattern does not match the string""" + assert_none(re.search(pattern, string), msg) + +def assert_mode_644(mode): + """Verify given mode is 644""" + assert (mode & stat.S_IROTH) and (mode & stat.S_IRGRP) + assert (mode & stat.S_IWUSR) and (mode & stat.S_IRUSR) and not (mode & stat.S_IXUSR) + +def assert_mode_755(mode): + """Verify given mode is 755""" + assert (mode & stat.S_IROTH) and (mode & stat.S_IRGRP) and (mode & stat.S_IXOTH) and (mode & stat.S_IXGRP) + assert (mode & stat.S_IWUSR) and (mode & stat.S_IRUSR) and (mode & stat.S_IXUSR)
\ No newline at end of file diff --git a/test/testlib/helper.py b/test/testlib/helper.py index ca262ee4..b66d3eaa 100644 --- a/test/testlib/helper.py +++ b/test/testlib/helper.py @@ -9,11 +9,22 @@ import os GIT_REPO = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) def fixture_path(name): - test_dir = os.path.dirname(os.path.dirname(__file__)) - return os.path.join(test_dir, "fixtures", name) + test_dir = os.path.dirname(os.path.dirname(__file__)) + return os.path.join(test_dir, "fixtures", name) def fixture(name): - return open(fixture_path(name)).read() + return open(fixture_path(name)).read() def absolute_project_path(): - return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + + +class ListProcessAdapter(object): + """Allows to use lists as Process object as returned by SubProcess.Popen. + Its tailored to work with the test system only""" + + def __init__(self, input_list_or_string): + l = input_list_or_string + if isinstance(l,basestring): + l = l.splitlines() + self.stdout = iter(l) |