From f2834177c0fdf6b1af659e460fd3348f468b8ab0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Oct 2009 11:50:14 +0200 Subject: Reorganized package structure and cleaned up imports --- CHANGES | 10 ++ lib/git/__init__.py | 8 +- lib/git/base.py | 317 -------------------------------------------- lib/git/blob.py | 126 ------------------ lib/git/commit.py | 284 --------------------------------------- lib/git/diff.py | 2 +- lib/git/head.py | 49 ------- lib/git/objects/__init__.py | 11 ++ lib/git/objects/base.py | 162 ++++++++++++++++++++++ lib/git/objects/blob.py | 36 +++++ lib/git/objects/commit.py | 284 +++++++++++++++++++++++++++++++++++++++ lib/git/objects/tag.py | 71 ++++++++++ lib/git/objects/tree.py | 102 ++++++++++++++ lib/git/objects/util.py | 36 +++++ lib/git/refs.py | 241 +++++++++++++++++++++++++++++++++ lib/git/repo.py | 98 +++++++++++++- lib/git/tag.py | 126 ------------------ lib/git/tree.py | 102 -------------- test/git/test_base.py | 12 +- test/git/test_blob.py | 28 ---- test/git/test_repo.py | 27 ++++ test/git/test_tag.py | 2 +- 22 files changed, 1085 insertions(+), 1049 deletions(-) delete mode 100644 lib/git/base.py delete mode 100644 lib/git/blob.py delete mode 100644 lib/git/commit.py delete mode 100644 lib/git/head.py create mode 100644 lib/git/objects/__init__.py create mode 100644 lib/git/objects/base.py create mode 100644 lib/git/objects/blob.py create mode 100644 lib/git/objects/commit.py create mode 100644 lib/git/objects/tag.py create mode 100644 lib/git/objects/tree.py create mode 100644 lib/git/objects/util.py create mode 100644 lib/git/refs.py delete mode 100644 lib/git/tag.py delete mode 100644 lib/git/tree.py diff --git a/CHANGES b/CHANGES index 5efcfbf2..69eddacc 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,16 @@ General where unique * removed basename method from Objects with path's as it replicated features of os.path +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. + Diff ---- * Members a a_commit and b_commit renamed to a_blob and b_blob - they are populated diff --git a/lib/git/__init__.py b/lib/git/__init__.py index 5ce3c122..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,TagRef,TagObject -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)) ] diff --git a/lib/git/base.py b/lib/git/base.py deleted file mode 100644 index 252ebe4b..00000000 --- a/lib/git/base.py +++ /dev/null @@ -1,317 +0,0 @@ -# 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 - -class LazyMixin(object): - lazy_properties = [] - __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 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": - self.size = int(self.repo.git.cat_file(self.id, s=True).rstrip()) - elif attr == "data": - self.data = self.repo.git.cat_file(self.id, p=True, with_raw_output=True) - - 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 '' % (self.__class__.__name__, self.id) - - @classmethod - def get_type_by_name(cls, 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) - - -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) - - @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 - - -class Ref(object): - """ - Represents a named reference to any object - """ - __slots__ = ("path", "object") - - def __init__(self, path, object = None): - """ - Initialize this instance - - ``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.path = path - self.object = object - - def __str__(self): - return self.name - - def __repr__(self): - return '' % (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:]) - - @classmethod - def find_all(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.list_from_string(repo, output) - - @classmethod - def list_from_string(cls, repo, text): - """ - Parse out ref information into a list of Ref compatible objects - - ``repo`` - is the Repo - ``text`` - is the text output from the git-for-each-ref command - - Returns - git.Ref[] - - list of Ref objects - """ - 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 Ref instance from the given string. - - ``repo`` - is the Repo - - ``line`` - is the formatted ref information - - Format:: - - name: [a-zA-Z_/]+ - - id: [0-9A-Fa-f]{40} - - Returns - git.Head - """ - full_path, hexsha, type_name, object_size = line.split("\x00") - obj = Object.get_type_by_name(type_name)(repo, hexsha) - obj.size = object_size - return cls(full_path, obj) diff --git a/lib/git/blob.py b/lib/git/blob.py deleted file mode 100644 index 1fafb128..00000000 --- a/lib/git/blob.py +++ /dev/null @@ -1,126 +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 -import base - -class Blob(base.IndexObject): - """A Blob encapsulates a git blob object""" - DEFAULT_MIME_TYPE = "text/plain" - type = "blob" - - __slots__ = tuple() - - # 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(.*)$') - - @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 - - @classmethod - def blame(cls, repo, commit, file): - """ - The blame information for the given file at the given commit - - Returns - list: [git.Commit, list: []] - 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(False): - parts = cls.re_whitespace.split(line, 1) - firstpart = parts[0] - if cls.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 = cls.re_author_committer_start.search(firstpart) - if m: - # handles: - # author Tom Preston-Werner - # author-mail - # author-time 1192271832 - # author-tz -0700 - # committer Tom Preston-Werner - # committer-mail - # 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 - # - 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( repo, 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 = cls.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 __repr__(self): - return '' % self.id diff --git a/lib/git/commit.py b/lib/git/commit.py deleted file mode 100644 index 68415be5..00000000 --- a/lib/git/commit.py +++ /dev/null @@ -1,284 +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 tree import Tree -import diff -import stats -import base - -class Commit(base.Object): - """ - 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 - - ``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) - # 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 self.__slots__: - 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 - 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 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 =text.splitlines(False) - 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]) - # END while there are parent lines - author, authored_date = cls._actor(lines.pop(0)) - committer, committed_date = cls._actor(lines.pop(0)) - - # free line - lines.pop(0) - - message_lines = [] - while lines and not lines[0].startswith('commit'): - message_lines.append(lines.pop(0).strip()) - # END while there are message lines - message = '\n'.join(message_lines[:-1]) # last line is empty - - commits.append(Commit(repo, id=id, parents=parents, tree=tree, author=author, authored_date=authored_date, - committer=committer, committed_date=committed_date, message=message)) - # END while lines - 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') - 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 '' % 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/diff.py b/lib/git/diff.py index 943fb08a..4bc88bf4 100644 --- a/lib/git/diff.py +++ b/lib/git/diff.py @@ -5,7 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php import re -import blob +import objects.blob as blob class Diff(object): """ diff --git a/lib/git/head.py b/lib/git/head.py deleted file mode 100644 index 42dfd735..00000000 --- a/lib/git/head.py +++ /dev/null @@ -1,49 +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 -import base - -class Head(base.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 - - - >>> head.commit.id - '1c09f116cbc2cb4100fb6935bb162daa4723f455' - """ - - @property - def commit(self): - """ - Returns - Commit object the head points to - """ - return self.object - - @classmethod - def find_all(cls, repo, common_path = "refs/heads", **kwargs): - """ - Returns - git.Head[] - - For more documentation, please refer to git.base.Ref.find_all - """ - return super(Head,cls).find_all(repo, common_path, **kwargs) - - def __repr__(self): - return '' % self.name 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..43aa8dd1 --- /dev/null +++ b/lib/git/objects/base.py @@ -0,0 +1,162 @@ +# 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 + +class LazyMixin(object): + lazy_properties = [] + __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 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": + self.size = int(self.repo.git.cat_file(self.id, s=True).rstrip()) + elif attr == "data": + self.data = self.repo.git.cat_file(self.id, p=True, with_raw_output=True) + + 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 '' % (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) + + @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 '' % self.id diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py new file mode 100644 index 00000000..c70b03e4 --- /dev/null +++ b/lib/git/objects/commit.py @@ -0,0 +1,284 @@ +# 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.actor import Actor +from tree import Tree +import git.diff as diff +import git.stats as stats +import base + +class Commit(base.Object): + """ + 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 + + ``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) + # 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 self.__slots__: + 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 + 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 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 =text.splitlines(False) + 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]) + # END while there are parent lines + author, authored_date = cls._actor(lines.pop(0)) + committer, committed_date = cls._actor(lines.pop(0)) + + # free line + lines.pop(0) + + message_lines = [] + while lines and not lines[0].startswith('commit'): + message_lines.append(lines.pop(0).strip()) + # END while there are message lines + message = '\n'.join(message_lines[:-1]) # last line is empty + + commits.append(Commit(repo, id=id, parents=parents, tree=tree, author=author, authored_date=authored_date, + committer=committer, committed_date=committed_date, message=message)) + # END while lines + 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') + 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 '' % 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..af1022f0 --- /dev/null +++ b/lib/git/objects/tag.py @@ -0,0 +1,71 @@ +# 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 util 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 self.__slots__: + output = self.repo.git.cat_file(self.type,self.id) + lines = output.split("\n") + + obj, hexsha = lines[0].split(" ") # object + type_token, type_name = lines[1].split(" ") # type + self.object = get_object_type_by_name(type_name)(self.repo, hexsha) + + self.tag = lines[2][4:] # tag + + tagger_info = lines[3][7:]# tagger + 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..597668ae --- /dev/null +++ b/lib/git/objects/tree.py @@ -0,0 +1,102 @@ +# 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 + +class Tree(base.IndexObject): + + type = "tree" + __slots__ = "_contents" + + def __init__(self, repo, id, mode=None, path=None): + super(Tree, self).__init__(repo, id, mode, path) + + def _set_cache_(self, attr): + if attr == "_contents": + # 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.path] = obj + else: + super(Tree, self)._set_cache_(attr) + + @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, path = text.expandtabs(1).split(" ", 3) + except: + return None + + if typ == "tree": + return Tree(repo, id, mode, path) + elif typ == "blob": + return blob.Blob(repo, id, mode, path) + 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' + + >>> Repo('/path/to/python-git').tree/'README.txt' + + + Returns + ``git.Blob`` or ``git.Tree`` or ``None`` if not found + """ + return self.get(file) + + + def __repr__(self): + return '' % 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/objects/util.py b/lib/git/objects/util.py new file mode 100644 index 00000000..15c1d114 --- /dev/null +++ b/lib/git/objects/util.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..cb730edb --- /dev/null +++ b/lib/git/refs.py @@ -0,0 +1,241 @@ +# 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.util import get_object_type_by_name + +class Ref(object): + """ + Represents a named reference to any object + """ + __slots__ = ("path", "object") + + def __init__(self, path, object = None): + """ + Initialize this instance + + ``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.path = path + self.object = object + + def __str__(self): + return self.name + + def __repr__(self): + return '' % (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:]) + + @classmethod + def find_all(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.list_from_string(repo, output) + + @classmethod + def list_from_string(cls, repo, text): + """ + Parse out ref information into a list of Ref compatible objects + + ``repo`` + is the Repo + ``text`` + is the text output from the git-for-each-ref command + + Returns + git.Ref[] + + list of Ref objects + """ + 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 Ref instance from the given string. + + ``repo`` + is the Repo + + ``line`` + is the formatted ref information + + Format:: + + name: [a-zA-Z_/]+ + + id: [0-9A-Fa-f]{40} + + Returns + git.Head + """ + full_path, hexsha, type_name, object_size = line.split("\x00") + obj = get_object_type_by_name(type_name)(repo, hexsha) + obj.size = object_size + return cls(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 + + + >>> head.commit.id + '1c09f116cbc2cb4100fb6935bb162daa4723f455' + """ + + @property + def commit(self): + """ + Returns + Commit object the head points to + """ + return self.object + + @classmethod + def find_all(cls, repo, common_path = "refs/heads", **kwargs): + """ + Returns + git.Head[] + + For more documentation, please refer to git.base.Ref.find_all + """ + return super(Head,cls).find_all(repo, common_path, **kwargs) + + def __repr__(self): + return '' % 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.find_all(repo)[0] + print tagref.commit.message + if tagref.tag is not None: + print tagref.tag.message + """ + + __slots__ = "tag" + + def __init__(self, path, commit_or_tag): + """ + Initialize a newly instantiated Tag + + ``path`` + is the full path to the tag + + ``commit_or_tag`` + is the Commit or TagObject that this tag ref points to + """ + super(TagRef, self).__init__(path, commit_or_tag) + self.tag = None + + if commit_or_tag.type == "tag": + self.tag = commit_or_tag + # END tag object handling + + @property + def commit(self): + """ + Returns + Commit object the tag ref points to + """ + if self.object.type == "commit": + return self.object + # it is a tag object + return self.object.object + + @classmethod + def find_all(cls, repo, common_path = "refs/tags", **kwargs): + """ + Returns + git.Tag[] + + For more documentation, please refer to git.base.Ref.find_all + """ + return super(TagRef,cls).find_all(repo, common_path, **kwargs) + + +# provide an alias +Tag = TagRef diff --git a/lib/git/repo.py b/lib/git/repo.py index 3c872218..6e23dbc6 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -5,16 +5,18 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php 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): """ @@ -23,6 +25,12 @@ class Repo(object): 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): """ @@ -108,6 +116,86 @@ class Repo(object): ``git.Tag[]`` """ return Tag.find_all(self) + + def blame(self, commit, file): + """ + The blame information for the given file at the given commit + + Returns + list: [git.Commit, list: []] + 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 + # author-time 1192271832 + # author-tz -0700 + # committer Tom Preston-Werner + # committer-mail + # 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 + # + 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=10, skip=0): """ diff --git a/lib/git/tag.py b/lib/git/tag.py deleted file mode 100644 index 89060ee0..00000000 --- a/lib/git/tag.py +++ /dev/null @@ -1,126 +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 - -import commit -import base - -class TagRef(base.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.find_all(repo)[0] - print tagref.commit.message - if tagref.tag is not None: - print tagref.tag.message - """ - - __slots__ = "tag" - - def __init__(self, path, commit_or_tag): - """ - Initialize a newly instantiated Tag - - ``path`` - is the full path to the tag - - ``commit_or_tag`` - is the Commit or TagObject that this tag ref points to - """ - super(TagRef, self).__init__(path, commit_or_tag) - self.tag = None - - if commit_or_tag.type == "tag": - self.tag = commit_or_tag - # END tag object handling - - @property - def commit(self): - """ - Returns - Commit object the tag ref points to - """ - if self.object.type == "commit": - return self.object - # it is a tag object - return self.object.object - - @classmethod - def find_all(cls, repo, common_path = "refs/tags", **kwargs): - """ - Returns - git.Tag[] - - For more documentation, please refer to git.base.Ref.find_all - """ - return super(TagRef,cls).find_all(repo, common_path, **kwargs) - - -# provide an alias -Tag = TagRef - -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 self.__slots__: - output = self.repo.git.cat_file(self.type,self.id) - lines = output.split("\n") - - obj, hexsha = lines[0].split(" ") # object - type_token, type_name = lines[1].split(" ") # type - self.object = base.Object.get_type_by_name(type_name)(self.repo, hexsha) - - self.tag = lines[2][4:] # tag - - tagger_info = lines[3][7:]# tagger - 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/tree.py b/lib/git/tree.py deleted file mode 100644 index 597668ae..00000000 --- a/lib/git/tree.py +++ /dev/null @@ -1,102 +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 -import blob -import base - -class Tree(base.IndexObject): - - type = "tree" - __slots__ = "_contents" - - def __init__(self, repo, id, mode=None, path=None): - super(Tree, self).__init__(repo, id, mode, path) - - def _set_cache_(self, attr): - if attr == "_contents": - # 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.path] = obj - else: - super(Tree, self)._set_cache_(attr) - - @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, path = text.expandtabs(1).split(" ", 3) - except: - return None - - if typ == "tree": - return Tree(repo, id, mode, path) - elif typ == "blob": - return blob.Blob(repo, id, mode, path) - 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' - - >>> Repo('/path/to/python-git').tree/'README.txt' - - - Returns - ``git.Blob`` or ``git.Tree`` or ``None`` if not found - """ - return self.get(file) - - - def __repr__(self): - return '' % 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/test/git/test_base.py b/test/git/test_base.py index 8f522cec..a153eb83 100644 --- a/test/git/test_base.py +++ b/test/git/test_base.py @@ -7,8 +7,10 @@ import time from test.testlib import * from git import * -import git.base as base +import git.objects.base as base +import git.refs as refs from itertools import chain +from git.objects.util import get_object_type_by_name class TestBase(object): @@ -60,7 +62,7 @@ class TestBase(object): ref_count = 0 for ref in chain(self.repo.tags, self.repo.heads): ref_count += 1 - assert isinstance(ref, base.Ref) + assert isinstance(ref, refs.Ref) assert str(ref) == ref.name assert repr(ref) assert ref == ref @@ -69,10 +71,10 @@ class TestBase(object): # END for each ref assert len(s) == ref_count - def test_get_type_by_name(self): + def test_get_object_type_by_name(self): for tname in base.Object.TYPES: - assert base.Object in base.Object.get_type_by_name(tname).mro() + assert base.Object in get_object_type_by_name(tname).mro() # END for each known type - assert_raises( ValueError, base.Object.get_type_by_name, "doesntexist" ) + assert_raises( ValueError, get_object_type_by_name, "doesntexist" ) diff --git a/test/git/test_blob.py b/test/git/test_blob.py index 94d3a33b..ebb53d0c 100644 --- a/test/git/test_blob.py +++ b/test/git/test_blob.py @@ -65,34 +65,6 @@ class TestBlob(object): blob = Blob(self.repo, **{'id': 'abc','path': 'something'}) assert_equal("text/plain", 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_should_return_appropriate_representation(self): blob = Blob(self.repo, **{'id': 'abc'}) assert_equal('', repr(blob)) diff --git a/test/git/test_repo.py b/test/git/test_repo.py index 421b8256..3e2fb3dc 100644 --- a/test/git/test_repo.py +++ b/test/git/test_repo.py @@ -257,3 +257,30 @@ class TestRepo(object): 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(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_tag.py b/test/git/test_tag.py index fe3f78cc..2ebb860a 100644 --- a/test/git/test_tag.py +++ b/test/git/test_tag.py @@ -7,7 +7,7 @@ from mock import * from test.testlib import * from git import * -from git.tag import TagObject +from git.objects.tag import TagObject import time class TestTag(object): -- cgit v1.2.1