From 58d692e2a1d7e3894dbed68efbcf7166d6ec3fb7 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 10:33:13 +0200 Subject: All times are not stored as time_struct, but as simple int to consume less memory time imports cleaned up and mostly removed as they were not required (anymore) --- lib/git/objects/commit.py | 31 +++++++++---------------------- lib/git/objects/tag.py | 12 ++++++------ lib/git/objects/utils.py | 19 +++++++++++++++++++ lib/git/repo.py | 11 ++++++----- 4 files changed, 40 insertions(+), 33 deletions(-) (limited to 'lib/git') diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py index 101014ab..edbe8ed7 100644 --- a/lib/git/objects/commit.py +++ b/lib/git/objects/commit.py @@ -4,14 +4,12 @@ # 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 +import utils class Commit(base.Object, Iterable): """ @@ -20,8 +18,6 @@ class Commit(base.Object, Iterable): 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" @@ -48,14 +44,16 @@ class Commit(base.Object, Iterable): ``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 + ``authored_date`` : int_seconds_since_epoch + is the authored DateTime - use time.gmtime() to convert it into a + different format ``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 + ``committed_date`` : int_seconds_since_epoch + is the committed DateTime - use time.gmtime() to convert it into a + different format ``message`` : string is the commit message @@ -185,8 +183,8 @@ class Commit(base.Object, Iterable): 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()) + author, authored_date = utils.parse_actor_and_date(next_line) + committer, committed_date = utils.parse_actor_and_date(stream.next()) # empty line stream.next() @@ -286,14 +284,3 @@ class Commit(base.Object, Iterable): 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 index ecf6349d..f54d4b64 100644 --- a/lib/git/objects/tag.py +++ b/lib/git/objects/tag.py @@ -7,8 +7,7 @@ Module containing all object based types. """ import base -import commit -from utils import get_object_type_by_name +import utils class TagObject(base.Object): """ @@ -38,8 +37,9 @@ class TagObject(base.Object): ``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 + ``tagged_date`` : int_seconds_since_epoch + is the DateTime of the tag creation - use time.gmtime to convert + it into a different format """ super(TagObject, self).__init__(repo, id ) self._set_self_from_args_(locals()) @@ -53,12 +53,12 @@ class TagObject(base.Object): 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.object = utils.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) + self.tagger, self.tagged_date = utils.parse_actor_and_date(tagger_info) # line 4 empty - check git source to figure out purpose self.message = "\n".join(lines[5:]) diff --git a/lib/git/objects/utils.py b/lib/git/objects/utils.py index 15c1d114..9b9e0c52 100644 --- a/lib/git/objects/utils.py +++ b/lib/git/objects/utils.py @@ -6,7 +6,9 @@ """ Module for general utility functions """ +import re import commit, tag, blob, tree +from git.actor import Actor def get_object_type_by_name(object_type_name): """ @@ -34,3 +36,20 @@ def get_object_type_by_name(object_type_name): return tree.Tree else: raise ValueError("Cannot handle unknown object type: %s" % object_type_name) + + +# precompiled regex +_re_actor_epoch = re.compile(r'^.+? (.*) (\d+) .*$') + +def parse_actor_and_date(line): + """ + Parse out the actor (author or committer) info from a line like:: + + author Tom Preston-Werner 1191999972 -0700 + + Returns + [Actor, int_seconds_since_epoch] + """ + m = _re_actor_epoch.search(line) + actor, epoch = m.groups() + return (Actor._from_string(actor), int(epoch)) diff --git a/lib/git/repo.py b/lib/git/repo.py index c74c7e8d..1640fd32 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -8,7 +8,6 @@ import os import re import gzip import StringIO -import time from errors import InvalidGitRepositoryError, NoSuchPathError from utils import touch, is_git_dir @@ -160,7 +159,7 @@ class Repo(object): if firstpart.endswith('-mail'): info["%s_email" % role] = parts[-1] elif firstpart.endswith('-time'): - info["%s_date" % role] = time.gmtime(int(parts[-1])) + info["%s_date" % role] = int(parts[-1]) elif role == firstpart: info[role] = parts[-1] # END distinguish mail,time,name @@ -197,12 +196,13 @@ class Repo(object): # END distinguish hexsha vs other information return blames - def commits(self, start='master', path='', max_count=None, skip=0): + def commits(self, start=None, 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') + is a ref to start the commits from. If start is None, + the active branch will be used ``path`` is an optional path to limit the returned commits to @@ -223,7 +223,8 @@ class Repo(object): if max_count is None: options.pop('max_count') - + if start is None: + start = self.active_branch return Commit.list_items(self, start, path, **options) def commits_between(self, frm, to): -- cgit v1.2.1 From 2da2b90e6930023ec5739ae0b714bbdb30874583 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 12:09:16 +0200 Subject: repo: removed a few methods because of redundancy or because it will be obsolete once the interface overhaul is finished. This commit is just intermediate --- lib/git/cmd.py | 18 ++++++++++- lib/git/objects/commit.py | 22 +++++++------- lib/git/repo.py | 76 ++++++++++++++--------------------------------- 3 files changed, 51 insertions(+), 65 deletions(-) (limited to 'lib/git') diff --git a/lib/git/cmd.py b/lib/git/cmd.py index 2965eb8b..ef7a9c6c 100644 --- a/lib/git/cmd.py +++ b/lib/git/cmd.py @@ -223,6 +223,21 @@ class Git(object): args.append("--%s=%s" % (dashify(k), v)) return args + @classmethod + def __unpack_args(cls, arg_list): + if not isinstance(arg_list, (list,tuple)): + return [ str(arg_list) ] + + outlist = list() + for arg in arg_list: + if isinstance(arg_list, (list, tuple)): + outlist.extend(cls.__unpack_args( arg )) + # END recursion + else: + outlist.append(str(arg)) + # END for each arg + return outlist + def _call_process(self, method, *args, **kwargs): """ Run the given git command with the specified arguments and return @@ -258,7 +273,8 @@ class Git(object): # Prepare the argument list opt_args = self.transform_kwargs(**kwargs) - ext_args = map(str, args) + + ext_args = self.__unpack_args(args) args = opt_args + ext_args call = ["git", dashify(method)] diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py index edbe8ed7..14839e12 100644 --- a/lib/git/objects/commit.py +++ b/lib/git/objects/commit.py @@ -102,7 +102,7 @@ class Commit(base.Object, Iterable): return self.message.split('\n', 1)[0] @classmethod - def count(cls, repo, ref, path=''): + def count(cls, repo, ref, paths=''): """ Count the number of commits reachable from this ref @@ -112,16 +112,17 @@ class Commit(base.Object, Iterable): ``ref`` is the ref from which to begin (SHA1 or name) - ``path`` - is an optinal path + ``paths`` + is an optinal path or a list of paths restricting the return value + to commits actually containing the paths Returns int """ - return len(repo.git.rev_list(ref, '--', path).strip().splitlines()) + return len(repo.git.rev_list(ref, '--', paths).strip().splitlines()) @classmethod - def iter_items(cls, repo, ref, path='', **kwargs): + def iter_items(cls, repo, ref, paths='', **kwargs): """ Find all commits matching the given criteria. @@ -131,14 +132,15 @@ class Commit(base.Object, Iterable): ``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 + ``paths`` + is an optinal path or list of paths, if set only Commits that include the path + or paths will be considered ``kwargs`` - optional keyword arguments to git where + optional keyword arguments to git rev-list where ``max_count`` is the maximum number of commits to fetch ``skip`` is the number of commits to skip + ``since`` all commits since i.e. '1970-01-01' Returns iterator yielding Commit items @@ -147,7 +149,7 @@ class Commit(base.Object, Iterable): options.update(kwargs) # the test system might confront us with string values - - proc = repo.git.rev_list(ref, '--', path, **options) + proc = repo.git.rev_list(ref, '--', paths, **options) return cls._iter_from_process_or_stream(repo, proc) @classmethod diff --git a/lib/git/repo.py b/lib/git/repo.py index 1640fd32..40c71fd8 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -116,9 +116,12 @@ class Repo(object): """ return Tag.list_items(self) - def blame(self, commit, file): + def blame(self, ref, file): """ - The blame information for the given file at the given commit + The blame information for the given file at the given ref. + + ``ref`` + Ref object or Commit Returns list: [git.Commit, list: []] @@ -126,7 +129,7 @@ class Repo(object): changed within the given commit. The Commit objects will be given in order of appearance. """ - data = self.git.blame(commit, '--', file, p=True) + data = self.git.blame(ref, '--', file, p=True) commits = {} blames = [] info = None @@ -196,17 +199,17 @@ class Repo(object): # END distinguish hexsha vs other information return blames - def commits(self, start=None, path='', max_count=None, skip=0): + def commits(self, start=None, paths='', max_count=None, skip=0): """ A list of Commit objects representing the history of a given ref/commit ``start`` - is a ref to start the commits from. If start is None, + is a Ref or Commit to start the commits from. If start is None, the active branch will be used - ``path`` - is an optional path to limit the returned commits to - Commits that do not contain that path will not be returned. + ``paths`` + is an optional path or a list of paths to limit the returned commits to + Commits that do not contain that path or the paths will not be returned. ``max_count`` is the maximum number of commits to return (default None) @@ -225,63 +228,27 @@ class Repo(object): options.pop('max_count') if start is None: start = self.active_branch - return Commit.list_items(self, start, path, **options) + + return Commit.list_items(self, start, paths, **options) - def commits_between(self, frm, to): + def commits_between(self, frm, to, *args, **kwargs): """ 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 + is the Ref/Commit name of the younger item ``to`` - is the branch/commit name of the older item + is the Ref/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 - - Returns - ``git.Commit[]`` - """ - options = {'since': since} - - return Commit.list_items(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') - - ``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=None, path = ''): + def commit(self, id=None, paths = ''): """ The Commit object for the specified id @@ -290,8 +257,9 @@ class Repo(object): if None, it defaults to the active branch - ``path`` - is an optional path, if set the returned commit must contain the path. + ``paths`` + is an optional path or a list of paths, + if set the returned commit must contain the path or paths Returns ``git.Commit`` @@ -300,7 +268,7 @@ class Repo(object): id = self.active_branch options = {'max_count': 1} - commits = Commit.list_items(self, id, path, **options) + commits = Commit.list_items(self, id, paths, **options) if not commits: raise ValueError, "Invalid identifier %s, or given path '%s' too restrictive" % ( id, path ) @@ -317,7 +285,7 @@ class Repo(object): other_repo_refs = other_repo.git.rev_list(other_ref, '--').strip().splitlines() diff_refs = list(set(other_repo_refs) - set(repo_refs)) - return map(lambda ref: Commit.list_items(other_repo, ref, max_count=1)[0], diff_refs) + return map(lambda ref: Commit(other_repo, ref ), diff_refs) def tree(self, treeish=None): """ -- cgit v1.2.1 From 9ce1193c851e98293a237ad3d2d87725c501e89f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 14:11:34 +0200 Subject: Added Commit.iter_parents to iterate all parents Renamed Commit.commits to iter_commits repo: assured proper use of the terms revision ( rev ) and reference ( ref ) --- lib/git/objects/commit.py | 42 ++++++++++++---- lib/git/refs.py | 6 +-- lib/git/repo.py | 120 +++++++++++++++++++++------------------------- 3 files changed, 89 insertions(+), 79 deletions(-) (limited to 'lib/git') diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py index 14839e12..ab8fdf26 100644 --- a/lib/git/objects/commit.py +++ b/lib/git/objects/commit.py @@ -100,17 +100,39 @@ class Commit(base.Object, Iterable): First line of the commit message. """ return self.message.split('\n', 1)[0] + + def iter_parents(self, paths='', **kwargs): + """ + Iterate _all_ parents of this commit. + + ``paths`` + Optional path or list of paths limiting the Commits to those that + contain at least one of the paths + + ``kwargs`` + All arguments allowed by git-rev-list + + Return: + Iterator yielding Commit objects which are parents of self + """ + # skip ourselves + skip = kwargs.get("skip", 1) + if skip == 0: # skip ourselves + skip = 1 + kwargs['skip'] = skip + + return self.iter_items( self.repo, self, paths, **kwargs ) @classmethod - def count(cls, repo, ref, paths=''): + def count(cls, repo, rev, paths=''): """ - Count the number of commits reachable from this ref + Count the number of commits reachable from this revision ``repo`` is the Repo - ``ref`` - is the ref from which to begin (SHA1 or name) + ``rev`` + revision specifier, see git-rev-parse for viable options ``paths`` is an optinal path or a list of paths restricting the return value @@ -119,18 +141,18 @@ class Commit(base.Object, Iterable): Returns int """ - return len(repo.git.rev_list(ref, '--', paths).strip().splitlines()) + return len(repo.git.rev_list(rev, '--', paths).strip().splitlines()) @classmethod - def iter_items(cls, repo, ref, paths='', **kwargs): + def iter_items(cls, repo, rev, paths='', **kwargs): """ Find all commits matching the given criteria. ``repo`` is the Repo - ``ref`` - is the ref from which to begin (SHA1, Head or name) + ``rev`` + revision specifier, see git-rev-parse for viable options ``paths`` is an optinal path or list of paths, if set only Commits that include the path @@ -149,7 +171,7 @@ class Commit(base.Object, Iterable): options.update(kwargs) # the test system might confront us with string values - - proc = repo.git.rev_list(ref, '--', paths, **options) + proc = repo.git.rev_list(rev, '--', paths, **options) return cls._iter_from_process_or_stream(repo, proc) @classmethod @@ -201,7 +223,7 @@ class Commit(base.Object, Iterable): # 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, + yield Commit(repo, id=id, parents=tuple(parents), tree=tree, author=author, authored_date=authored_date, committer=committer, committed_date=committed_date, message=message) # END for each line in stream diff --git a/lib/git/refs.py b/lib/git/refs.py index 3c9eb817..8ed578ef 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -10,7 +10,7 @@ from objects.base import Object from objects.utils import get_object_type_by_name from utils import LazyMixin, Iterable -class Ref(LazyMixin, Iterable): +class Reference(LazyMixin, Iterable): """ Represents a named reference to any object """ @@ -138,7 +138,7 @@ class Ref(LazyMixin, Iterable): # return cls(repo, full_path, obj) -class Head(Ref): +class Head(Reference): """ A Head is a named reference to a Commit. Every Head instance contains a name and a Commit object. @@ -181,7 +181,7 @@ class Head(Ref): -class TagRef(Ref): +class TagRef(Reference): """ 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 diff --git a/lib/git/repo.py b/lib/git/repo.py index 40c71fd8..9b947d60 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -116,12 +116,12 @@ class Repo(object): """ return Tag.list_items(self) - def blame(self, ref, file): + def blame(self, rev, file): """ - The blame information for the given file at the given ref. + The blame information for the given file at the given revision. - ``ref`` - Ref object or Commit + ``rev`` + revision specifier, see git-rev-parse for viable options. Returns list: [git.Commit, list: []] @@ -199,37 +199,29 @@ class Repo(object): # END distinguish hexsha vs other information return blames - def commits(self, start=None, paths='', max_count=None, skip=0): + def iter_commits(self, rev=None, paths='', **kwargs): """ A list of Commit objects representing the history of a given ref/commit - ``start`` - is a Ref or Commit to start the commits from. If start is None, - the active branch will be used + ``rev`` + revision specifier, see git-rev-parse for viable options. + If None, the active branch will be used. ``paths`` is an optional path or a list of paths to limit the returned commits to Commits that do not contain that path or the paths 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. + + ``kwargs`` + Arguments to be passed to git-rev-parse - common ones are + max_count and skip Returns ``git.Commit[]`` """ - options = {'max_count': max_count, - 'skip': skip} - - if max_count is None: - options.pop('max_count') - if start is None: - start = self.active_branch + if rev is None: + rev = self.active_branch - return Commit.list_items(self, start, paths, **options) + return Commit.list_items(self, rev, paths, **kwargs) def commits_between(self, frm, to, *args, **kwargs): """ @@ -248,50 +240,31 @@ class Repo(object): return reversed(Commit.list_items(self, "%s..%s" % (frm, to))) - def commit(self, id=None, paths = ''): + def commit(self, rev=None): """ - The Commit object for the specified id + The Commit object for the specified revision - ``id`` - is the SHA1 identifier of the commit or a ref or a ref name - if None, it defaults to the active branch + ``rev`` + revision specifier, see git-rev-parse for viable options. - - ``paths`` - is an optional path or a list of paths, - if set the returned commit must contain the path or paths - Returns ``git.Commit`` """ - if id is None: - id = self.active_branch - options = {'max_count': 1} - - commits = Commit.list_items(self, id, paths, **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 rev is None: + rev = self.active_branch + + # NOTE: currently we are not checking wheter rev really points to a commit + # If not, the system will barf on access of the object, but we don't do that + # here to safe cycles + c = Commit(self, rev) + return c - diff_refs = list(set(other_repo_refs) - set(repo_refs)) - return map(lambda ref: Commit(other_repo, ref ), diff_refs) - def tree(self, treeish=None): + def tree(self, ref=None): """ The Tree object for the given treeish reference - ``treeish`` + ``ref`` is a Ref instance defaulting to the active_branch if None. Examples:: @@ -305,27 +278,42 @@ class Repo(object): 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 ) + If you need a non-root level tree, find it by iterating the root tree. Otherwise + it cannot know about its path relative to the repository root and subsequent + operations might have unexpected results. + """ + if ref is None: + ref = self.active_branch + if not isinstance(ref, Reference): + raise ValueError( "Reference required, got %r" % ref ) # 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 ) + hexsha, typename, size = self.git.get_object_header( ref ) if typename != "tree": - hexsha, typename, size = self.git.get_object_header( str(treeish)+'^{tree}' ) + # will raise if this is not a valid tree + hexsha, typename, size = self.git.get_object_header( str(ref)+'^{tree}' ) # END tree handling - treeish = hexsha + ref = hexsha # the root has an empty relative path and the default mode - return Tree(self, treeish, 0, '') + return Tree(self, ref, 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() + + diff_refs = list(set(other_repo_refs) - set(repo_refs)) + return map(lambda ref: Commit(other_repo, ref ), diff_refs) def diff(self, a, b, *paths): """ -- cgit v1.2.1 From 9f51eeb2f9a1efe22e45f7a1f7b963100f2f8e6b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 14:56:20 +0200 Subject: repo: removed commits_between but added a note about how this can be achieved using the iter_commits method; reorganized methods within the type as a start for more interface changes --- lib/git/repo.py | 424 +++++++++++++++++++++++++++----------------------------- 1 file changed, 206 insertions(+), 218 deletions(-) (limited to 'lib/git') diff --git a/lib/git/repo.py b/lib/git/repo.py index 9b947d60..9ac6ce0c 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -116,130 +116,6 @@ class Repo(object): """ return Tag.list_items(self) - def blame(self, rev, file): - """ - The blame information for the given file at the given revision. - - ``rev`` - revision specifier, see git-rev-parse for viable options. - - 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(ref, '--', 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] = 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 iter_commits(self, rev=None, paths='', **kwargs): - """ - A list of Commit objects representing the history of a given ref/commit - - ``rev`` - revision specifier, see git-rev-parse for viable options. - If None, the active branch will be used. - - ``paths`` - is an optional path or a list of paths to limit the returned commits to - Commits that do not contain that path or the paths will not be returned. - - ``kwargs`` - Arguments to be passed to git-rev-parse - common ones are - max_count and skip - - Returns - ``git.Commit[]`` - """ - if rev is None: - rev = self.active_branch - - return Commit.list_items(self, rev, paths, **kwargs) - - def commits_between(self, frm, to, *args, **kwargs): - """ - The Commits objects that are reachable via ``to`` but not via ``frm`` - Commits are returned in chronological order. - - ``from`` - is the Ref/Commit name of the younger item - - ``to`` - is the Ref/Commit name of the older item - - Returns - ``git.Commit[]`` - """ - return reversed(Commit.list_items(self, "%s..%s" % (frm, to))) - - def commit(self, rev=None): """ The Commit object for the specified revision @@ -259,7 +135,6 @@ class Repo(object): c = Commit(self, rev) return c - def tree(self, ref=None): """ The Tree object for the given treeish reference @@ -302,6 +177,33 @@ class Repo(object): # the root has an empty relative path and the default mode return Tree(self, ref, 0, '') + def iter_commits(self, rev=None, paths='', **kwargs): + """ + A list of Commit objects representing the history of a given ref/commit + + ``rev`` + revision specifier, see git-rev-parse for viable options. + If None, the active branch will be used. + + ``paths`` + is an optional path or a list of paths to limit the returned commits to + Commits that do not contain that path or the paths will not be returned. + + ``kwargs`` + Arguments to be passed to git-rev-parse - common ones are + max_count and skip + + Note: to receive only commits between two named revisions, use the + "revA..revB" revision specifier + + Returns + ``git.Commit[]`` + """ + if rev is None: + rev = self.active_branch + + return Commit.iter_items(self, rev, paths, **kwargs) + 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 @@ -315,6 +217,102 @@ class Repo(object): diff_refs = list(set(other_repo_refs) - set(repo_refs)) return map(lambda ref: Commit(other_repo, ref ), diff_refs) + + 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`` + + 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 + Head to the active branch + """ + return Head( self, self.git.symbolic_ref('HEAD').strip() ) + + def diff(self, a, b, *paths): """ The diff from commit ``a`` to commit ``b``, optionally restricted to the given file(s) @@ -341,6 +339,89 @@ class Repo(object): ``git.Diff[]`` """ return Commit.diff(self, commit) + + def blame(self, rev, file): + """ + The blame information for the given file at the given revision. + + ``rev`` + revision specifier, see git-rev-parse for viable options. + + 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(rev, '--', 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] = 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 @classmethod def init_bare(self, path, mkdir=True, **kwargs): @@ -454,99 +535,6 @@ class Repo(object): 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`` - - 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 - Head to the active branch - """ - return Head( self, self.git.symbolic_ref('HEAD').strip() ) def __repr__(self): return '' % self.path -- cgit v1.2.1 From 39229db70e71a6be275dafb3dd9468930e40ae48 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 15:42:00 +0200 Subject: Object can now create objects of the proper type in case one attempts to create an object directly - this feature is used in several places now, allowing for additional type-checking --- lib/git/objects/base.py | 15 +++++++++++++++ lib/git/objects/utils.py | 1 - lib/git/refs.py | 4 ++-- lib/git/repo.py | 6 ++---- 4 files changed, 19 insertions(+), 7 deletions(-) (limited to 'lib/git') diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py index 07538ada..3b48e066 100644 --- a/lib/git/objects/base.py +++ b/lib/git/objects/base.py @@ -5,17 +5,32 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php import os from git.utils import LazyMixin +import utils _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 + + This Object also serves as a constructor for instances of the correct type:: + + inst = Object(repo,id) """ TYPES = ("blob", "tree", "commit", "tag") __slots__ = ("repo", "id", "size", "data" ) type = None # to be set by subclass + def __new__(cls, repo, id, *args, **kwargs): + if cls is Object: + hexsha, typename, size = repo.git.get_object_header(id) + obj_type = utils.get_object_type_by_name(typename) + inst = super(Object,cls).__new__(obj_type, repo, hexsha, *args, **kwargs) + inst.size = size + return inst + else: + return super(Object,cls).__new__(cls, repo, id, *args, **kwargs) + def __init__(self, repo, id): """ Initialize an object by identifying it by its id. All keyword arguments diff --git a/lib/git/objects/utils.py b/lib/git/objects/utils.py index 9b9e0c52..367ed2b7 100644 --- a/lib/git/objects/utils.py +++ b/lib/git/objects/utils.py @@ -7,7 +7,6 @@ Module for general utility functions """ import re -import commit, tag, blob, tree from git.actor import Actor def get_object_type_by_name(object_type_name): diff --git a/lib/git/refs.py b/lib/git/refs.py index 8ed578ef..a4d7bbb1 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -71,8 +71,8 @@ class Reference(LazyMixin, Iterable): 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) + # Our path will be resolved to the hexsha which will be used accordingly + return Object(self.repo, self.path) @classmethod def iter_items(cls, repo, common_path = "refs", **kwargs): diff --git a/lib/git/repo.py b/lib/git/repo.py index 9ac6ce0c..1d24edb7 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -129,10 +129,8 @@ class Repo(object): if rev is None: rev = self.active_branch - # NOTE: currently we are not checking wheter rev really points to a commit - # If not, the system will barf on access of the object, but we don't do that - # here to safe cycles - c = Commit(self, rev) + c = Object(self, rev) + assert c.type == "commit", "Revision %s did not point to a commit, but to %s" % (rev, c) return c def tree(self, ref=None): -- cgit v1.2.1 From 806450db9b2c4a829558557ac90bd7596a0654e0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 16:13:51 +0200 Subject: repo.commit_delta_base: removed --- lib/git/objects/commit.py | 153 +++++++++++++++++++++++----------------------- lib/git/repo.py | 14 ----- 2 files changed, 78 insertions(+), 89 deletions(-) (limited to 'lib/git') diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py index ab8fdf26..847f4dec 100644 --- a/lib/git/objects/commit.py +++ b/lib/git/objects/commit.py @@ -101,30 +101,8 @@ class Commit(base.Object, Iterable): """ return self.message.split('\n', 1)[0] - def iter_parents(self, paths='', **kwargs): - """ - Iterate _all_ parents of this commit. - - ``paths`` - Optional path or list of paths limiting the Commits to those that - contain at least one of the paths - - ``kwargs`` - All arguments allowed by git-rev-list - - Return: - Iterator yielding Commit objects which are parents of self - """ - # skip ourselves - skip = kwargs.get("skip", 1) - if skip == 0: # skip ourselves - skip = 1 - kwargs['skip'] = skip - - return self.iter_items( self.repo, self, paths, **kwargs ) - @classmethod - def count(cls, repo, rev, paths=''): + def count(cls, repo, rev, paths='', **kwargs): """ Count the number of commits reachable from this revision @@ -138,10 +116,12 @@ class Commit(base.Object, Iterable): is an optinal path or a list of paths restricting the return value to commits actually containing the paths + ``kwargs`` + Additional options to be passed to git-rev-list Returns int """ - return len(repo.git.rev_list(rev, '--', paths).strip().splitlines()) + return len(repo.git.rev_list(rev, '--', paths, **kwargs).strip().splitlines()) @classmethod def iter_items(cls, repo, rev, paths='', **kwargs): @@ -173,59 +153,28 @@ class Commit(base.Object, Iterable): # the test system might confront us with string values - proc = repo.git.rev_list(rev, '--', paths, **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 + + def iter_parents(self, paths='', **kwargs): """ - 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 = utils.parse_actor_and_date(next_line) - committer, committed_date = utils.parse_actor_and_date(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) + Iterate _all_ parents of this commit. + + ``paths`` + Optional path or list of paths limiting the Commits to those that + contain at least one of the paths + + ``kwargs`` + All arguments allowed by git-rev-list - yield Commit(repo, id=id, parents=tuple(parents), tree=tree, author=author, authored_date=authored_date, - committer=committer, committed_date=committed_date, message=message) - # END for each line in stream + Return: + Iterator yielding Commit objects which are parents of self + """ + # skip ourselves + skip = kwargs.get("skip", 1) + if skip == 0: # skip ourselves + skip = 1 + kwargs['skip'] = skip + + return self.iter_items( self.repo, self, paths, **kwargs ) @classmethod def diff(cls, repo, a, b=None, paths=None): @@ -301,6 +250,60 @@ class Commit(base.Object, Iterable): text = self.repo.git.diff(self.parents[0].id, self.id, '--', numstat=True) return stats.Stats._list_from_string(self.repo, text) + @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 = utils.parse_actor_and_date(next_line) + committer, committed_date = utils.parse_actor_and_date(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=tuple(parents), tree=tree, author=author, authored_date=authored_date, + committer=committer, committed_date=committed_date, message=message) + # END for each line in stream + + def __str__(self): """ Convert commit to string which is SHA1 """ return self.id diff --git a/lib/git/repo.py b/lib/git/repo.py index 1d24edb7..cfa73c43 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -202,20 +202,6 @@ class Repo(object): return Commit.iter_items(self, rev, paths, **kwargs) - 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() - - diff_refs = list(set(other_repo_refs) - set(repo_refs)) - return map(lambda ref: Commit(other_repo, ref ), diff_refs) - - def _get_daemon_export(self): filename = os.path.join(self.path, self.DAEMON_EXPORT_FILE) return os.path.exists(filename) -- cgit v1.2.1 From 00c5497f190172765cc7a53ff9d8852a26b91676 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 16:52:20 +0200 Subject: repo: made init and clone methods less specific, previously they wanted to do it 'barely' only. New method names closely follow the default git command names --- lib/git/repo.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'lib/git') diff --git a/lib/git/repo.py b/lib/git/repo.py index cfa73c43..39b1cb50 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -408,52 +408,52 @@ class Repo(object): return blames @classmethod - def init_bare(self, path, mkdir=True, **kwargs): + def init(cls, path=None, mkdir=True, **kwargs): """ - Initialize a bare git repository at the given path + Initialize a git repository at the given path if specified ``path`` is the full path to the repo (traditionally ends with /.git) + or None in which case the repository will be created in the current + working directory ``mkdir`` if specified will create the repository directory if it doesn't - already exists. Creates the directory with a mode=0755. + already exists. Creates the directory with a mode=0755. + Only effective if a path is explicitly given ``kwargs`` - keyword arguments serving as additional options to the git init command + keyword arguments serving as additional options to the git-init command Examples:: - git.Repo.init_bare('/var/git/myrepo.git') + git.Repo.init('/var/git/myrepo.git',bare=True) Returns ``git.Repo`` (the newly created repo) """ - if mkdir and not os.path.exists(path): + if mkdir and path and not os.path.exists(path): os.makedirs(path, 0755) git = Git(path) - output = git.init('--bare', **kwargs) + output = git.init(path, **kwargs) return Repo(path) - create = init_bare - def fork_bare(self, path, **kwargs): + def clone(self, path, **kwargs): """ - Fork a bare git repository from this repo + Create a clone from this repository. ``path`` is the full path of the new repo (traditionally ends with /.git) ``kwargs`` - keyword arguments to be given to the git clone command + keyword arguments to be given to the git-clone command Returns - ``git.Repo`` (the newly forked repo) + ``git.Repo`` (the newly cloned repo) """ - options = {'bare': True} - options.update(kwargs) - self.git.clone(self.path, path, **options) + self.git.clone(self.path, path, **kwargs) return Repo(path) def archive_tar(self, treeish='master', prefix=None): -- cgit v1.2.1 From b67bd4c730273a9b6cce49a8444fb54e654de540 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Oct 2009 18:05:28 +0200 Subject: Improved archive function by allowing it to directly write to an output stream - previously it would cache everything to memory and try to provide zipping functionality itself gitcmd: allows the output stream to be set explicitly which is mainly useful for archiving operations --- lib/git/cmd.py | 36 ++++++++++++++++++++----------- lib/git/repo.py | 67 +++++++++++++++++++++------------------------------------ 2 files changed, 48 insertions(+), 55 deletions(-) (limited to 'lib/git') diff --git a/lib/git/cmd.py b/lib/git/cmd.py index ef7a9c6c..500fcd93 100644 --- a/lib/git/cmd.py +++ b/lib/git/cmd.py @@ -13,7 +13,8 @@ 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', 'as_process') + 'with_exceptions', 'with_raw_output', 'as_process', + 'output_stream' ) extra = {} if sys.platform == 'win32': @@ -102,7 +103,8 @@ class Git(object): with_extended_output=False, with_exceptions=True, with_raw_output=False, - as_process=False + as_process=False, + output_stream=None ): """ Handles executing the command on the shell and consumes and returns @@ -130,16 +132,20 @@ class Git(object): ``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. - + ``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. + ``output_stream`` + If set to a file-like object, data produced by the git command will be + output to the given stream directly. + Otherwise a new file will be opened. + Returns:: str(output) # extended_output = False (Default) @@ -160,13 +166,17 @@ class Git(object): cwd = os.getcwd() else: cwd=self.git_dir + + ostream = subprocess.PIPE + if output_stream is not None: + ostream = output_stream # Start the process proc = subprocess.Popen(command, cwd=cwd, stdin=istream, stderr=subprocess.PIPE, - stdout=subprocess.PIPE, + stdout=ostream, **extra ) diff --git a/lib/git/repo.py b/lib/git/repo.py index 39b1cb50..554c10cb 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -456,48 +456,27 @@ class Repo(object): self.git.clone(self.path, path, **kwargs) 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 - - - >>> repo.archive_tar('a87ff14') - - >>> repo.archive_tar('master', 'myproject/') - - - Returns - str (containing bytes of tar archive) + def archive(self, ostream, treeish=None, prefix=None, **kwargs): """ - 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 + Archive the tree at the given revision. + ``ostream`` + file compatible stream object to which the archive will be written ``treeish`` - is the treeish name/id (default 'master') + is the treeish name/id, defaults to active branch ``prefix`` is the optional prefix to prepend to each filename in the archive + + ``kwargs`` + Additional arguments passed to git-archive + NOTE: Use the 'format' argument to define the kind of format. Use + specialized ostreams to write any format supported by python Examples:: - >>> repo.archive_tar_gz + >>> repo.archive(open("archive" >>> repo.archive_tar_gz('a87ff14') @@ -506,18 +485,22 @@ class Repo(object): >>> repo.archive_tar_gz('master', 'myproject/') - Returns - str (containing the bytes of tar.gz archive) + Raise + GitCommandError in case something went wrong + """ - kwargs = {} - if prefix: + if treeish is None: + treeish = self.active_branch + if prefix and 'prefix' not in kwargs: 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() + kwargs['as_process'] = True + kwargs['output_stream'] = ostream + + proc = self.git.archive(treeish, **kwargs) + status = proc.wait() + if status != 0: + raise GitCommandError( "git-archive", status, proc.stderr.read() ) + def __repr__(self): -- cgit v1.2.1