From 22757ed7b58862cccef64fdc09f93ea1ac72b1d2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 27 Oct 2009 11:58:20 +0100 Subject: put _make_file helper method into TestBase class remote: prepared FetchInfo class to be returned by fetch and pull. About to implement tests --- lib/git/remote.py | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 7febf2ee..12394c6f 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -49,6 +49,38 @@ class Remote(LazyMixin, Iterable): __slots__ = ( "repo", "name", "_config_reader" ) _id_attribute_ = "name" + class FetchInfo(object): + """ + Carries information about the results of a fetch operation:: + + info = remote.fetch()[0] + info.local_ref # None, or Reference object to the local head or tag which was moved + info.remote_ref # Symbolic Reference or RemoteReference to the changed remote head or FETCH_HEAD + info.flags # additional flags to be & with enumeration members, i.e. info.flags & info.REJECTED + """ + __slots__ = tuple() + BRANCH_UPTODATE, REJECTED, FORCED_UPDATED, FAST_FORWARD, NEW_TAG, \ + TAG_UPDATE, NEW_BRANCH = [ 1 << x for x in range(1,8) ] + + def __init__(self, local_ref, remote_ref, flags): + """ + Initialize a new instance + """ + self.local_ref = local_ref + self.remote_ref = remote_ref + self.flags = flags + + @classmethod + def _from_line(cls, line): + """ + Parse information from the given line as returned by git-fetch -v + and return a new FetchInfo object representing this information. + """ + raise NotImplementedError("todo") + + # END FetchInfo definition + + def __init__(self, repo, name): """ Initialize a remote instance @@ -218,10 +250,11 @@ class Remote(LazyMixin, Iterable): Additional arguments to be passed to git-fetch Returns - self + list(FetchInfo, ...) list of FetchInfo instances providing detailed + information about the fetch results """ - self.repo.git.fetch(self, refspec, **kwargs) - return self + lines = self.repo.git.fetch(self, refspec, v=True, **kwargs).splitlines() + return [ self.FetchInfo._from_line(line) for line in lines ] def pull(self, refspec=None, **kwargs): """ @@ -235,10 +268,10 @@ class Remote(LazyMixin, Iterable): Additional arguments to be passed to git-pull Returns - self + list(Fetch """ - self.repo.git.pull(self, refspec, **kwargs) - return self + lines = self.repo.git.pull(self, refspec, v=True, **kwargs).splitlines() + return [ self.FetchInfo._from_line(line) for line in lines ] def push(self, refspec=None, **kwargs): """ -- cgit v1.2.1 From 5047344a22ed824735d6ed1c91008767ea6638b7 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 27 Oct 2009 20:09:50 +0100 Subject: Added testing frame for proper fetch testing to be very sure this works as expected. Plenty of cases still to be tested --- lib/git/remote.py | 73 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 13 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 12394c6f..90b43467 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -8,7 +8,9 @@ Module implementing a remote object allowing easy access to git remotes """ from git.utils import LazyMixin, Iterable, IterableList -from refs import RemoteReference +from refs import Reference, RemoteReference +import re +import os class _SectionConstraint(object): """ @@ -54,29 +56,70 @@ class Remote(LazyMixin, Iterable): Carries information about the results of a fetch operation:: info = remote.fetch()[0] - info.local_ref # None, or Reference object to the local head or tag which was moved info.remote_ref # Symbolic Reference or RemoteReference to the changed remote head or FETCH_HEAD info.flags # additional flags to be & with enumeration members, i.e. info.flags & info.REJECTED + info.note # additional notes given by git-fetch intended for the user """ - __slots__ = tuple() - BRANCH_UPTODATE, REJECTED, FORCED_UPDATED, FAST_FORWARD, NEW_TAG, \ - TAG_UPDATE, NEW_BRANCH = [ 1 << x for x in range(1,8) ] + __slots__ = ('remote_ref', 'flags', 'note') + BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ + TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] + # %c %-*s %-*s -> %s (%s) + re_fetch_result = re.compile("^(.) (\[?[\w\s]+\]?)\s+(.+) -> (.+/.+)( \(.*\)?$)?") - def __init__(self, local_ref, remote_ref, flags): + _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, + '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } + + def __init__(self, remote_ref, flags, note = ''): """ Initialize a new instance """ - self.local_ref = local_ref self.remote_ref = remote_ref self.flags = flags + self.note = note @classmethod - def _from_line(cls, line): + def _from_line(cls, repo, line): """ Parse information from the given line as returned by git-fetch -v and return a new FetchInfo object representing this information. + + We can handle a line as follows + "%c %-*s %-*s -> %s%s" + + Where c is either ' ', !, +, -, *, or = + ! means error + + means success forcing update + - means a tag was updated + * means birth of new branch or tag + = means the head was up to date ( and not moved ) + ' ' means a fast-forward """ - raise NotImplementedError("todo") + line = line.strip() + match = cls.re_fetch_result.match(line) + if match is None: + raise ValueError("Failed to parse line: %r" % line) + control_character, operation, local_remote_ref, remote_local_ref, note = match.groups() + + remote_local_ref = Reference.from_path(repo, os.path.join(RemoteReference._common_path_default, remote_local_ref.strip())) + note = ( note and note.strip() ) or '' + + # parse flags from control_character + flags = 0 + try: + flags |= cls._flag_map[control_character] + except KeyError: + raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) + # END control char exception hanlding + + # parse operation string for more info + if 'rejected' in operation: + flags |= cls.REJECTED + if 'new tag' in operation: + flags |= cls.NEW_TAG + if 'new branch' in operation: + flags |= cls.NEW_BRANCH + + return cls(remote_local_ref, flags, note) # END FetchInfo definition @@ -230,6 +273,10 @@ class Remote(LazyMixin, Iterable): self.repo.git.remote("update", self.name) return self + def _get_fetch_info_from_stderr(self, stderr): + # skip first line as it is some remote info we are not interested in + return [ self.FetchInfo._from_line(self.repo, line) for line in stderr.splitlines()[1:] ] + def fetch(self, refspec=None, **kwargs): """ Fetch the latest changes for this remote @@ -253,8 +300,8 @@ class Remote(LazyMixin, Iterable): list(FetchInfo, ...) list of FetchInfo instances providing detailed information about the fetch results """ - lines = self.repo.git.fetch(self, refspec, v=True, **kwargs).splitlines() - return [ self.FetchInfo._from_line(line) for line in lines ] + status, stdout, stderr = self.repo.git.fetch(self, refspec, with_extended_output=True, v=True, **kwargs) + return self._get_fetch_info_from_stderr(stderr) def pull(self, refspec=None, **kwargs): """ @@ -270,8 +317,8 @@ class Remote(LazyMixin, Iterable): Returns list(Fetch """ - lines = self.repo.git.pull(self, refspec, v=True, **kwargs).splitlines() - return [ self.FetchInfo._from_line(line) for line in lines ] + status, stdout, stderr = self.repo.git.pull(self, refspec, v=True, with_extended_output=True, **kwargs) + return self._get_fetch_info_from_stderr(stderr) def push(self, refspec=None, **kwargs): """ -- cgit v1.2.1 From 038f183313f796dc0313c03d652a2bcc1698e78e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 27 Oct 2009 20:46:26 +0100 Subject: implemented test for rejection handling and fixed a bug when parsing remote reference paths --- lib/git/remote.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 90b43467..36c71e7a 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -8,6 +8,7 @@ Module implementing a remote object allowing easy access to git remotes """ from git.utils import LazyMixin, Iterable, IterableList +from objects import Commit from refs import Reference, RemoteReference import re import os @@ -58,24 +59,39 @@ class Remote(LazyMixin, Iterable): info = remote.fetch()[0] info.remote_ref # Symbolic Reference or RemoteReference to the changed remote head or FETCH_HEAD info.flags # additional flags to be & with enumeration members, i.e. info.flags & info.REJECTED - info.note # additional notes given by git-fetch intended for the user + info.note # additional notes given by git-fetch intended for the user + info.commit_before_forced_update # if info.flags & info.FORCED_UPDATE, field is set to the + # previous location of remote_ref, otherwise None """ - __slots__ = ('remote_ref', 'flags', 'note') + __slots__ = ('remote_ref','commit_before_forced_update', 'flags', 'note') + BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] # %c %-*s %-*s -> %s (%s) - re_fetch_result = re.compile("^(.) (\[?[\w\s]+\]?)\s+(.+) -> (.+/.+)( \(.*\)?$)?") + re_fetch_result = re.compile("^(.) (\[?[\w\s\.]+\]?)\s+(.+) -> (.+/[\w_\.-]+)( \(.*\)?$)?") _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } - def __init__(self, remote_ref, flags, note = ''): + def __init__(self, remote_ref, flags, note = '', old_commit = None): """ Initialize a new instance """ self.remote_ref = remote_ref self.flags = flags self.note = note + self.commit_before_forced_update = old_commit + + def __str__(self): + return self.name + + @property + def name(self): + """ + Returns + Name of our remote ref + """ + return self.remote_ref.name @classmethod def _from_line(cls, repo, line): @@ -112,14 +128,18 @@ class Remote(LazyMixin, Iterable): # END control char exception hanlding # parse operation string for more info + old_commit = None if 'rejected' in operation: flags |= cls.REJECTED if 'new tag' in operation: flags |= cls.NEW_TAG if 'new branch' in operation: flags |= cls.NEW_BRANCH + if '...' in operation: + old_commit = Commit(repo, operation.split('...')[0]) + # END handle refspec - return cls(remote_local_ref, flags, note) + return cls(remote_local_ref, flags, note, old_commit) # END FetchInfo definition @@ -275,7 +295,10 @@ class Remote(LazyMixin, Iterable): def _get_fetch_info_from_stderr(self, stderr): # skip first line as it is some remote info we are not interested in - return [ self.FetchInfo._from_line(self.repo, line) for line in stderr.splitlines()[1:] ] + print stderr + output = IterableList('name') + output.extend(self.FetchInfo._from_line(self.repo, line) for line in stderr.splitlines()[1:]) + return output def fetch(self, refspec=None, **kwargs): """ @@ -297,7 +320,7 @@ class Remote(LazyMixin, Iterable): Additional arguments to be passed to git-fetch Returns - list(FetchInfo, ...) list of FetchInfo instances providing detailed + IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed information about the fetch results """ status, stdout, stderr = self.repo.git.fetch(self, refspec, with_extended_output=True, v=True, **kwargs) @@ -315,7 +338,7 @@ class Remote(LazyMixin, Iterable): Additional arguments to be passed to git-pull Returns - list(Fetch + Please see 'fetch' method """ status, stdout, stderr = self.repo.git.pull(self, refspec, v=True, with_extended_output=True, **kwargs) return self._get_fetch_info_from_stderr(stderr) -- cgit v1.2.1 From 138aa2b8b413a19ebf9b2bbb39860089c4436001 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 27 Oct 2009 20:57:54 +0100 Subject: Added non-fast forward test case, fixed parsing issue caused by initial line stripping --- lib/git/remote.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 36c71e7a..d4ca9eb3 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -68,7 +68,7 @@ class Remote(LazyMixin, Iterable): BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] # %c %-*s %-*s -> %s (%s) - re_fetch_result = re.compile("^(.) (\[?[\w\s\.]+\]?)\s+(.+) -> (.+/[\w_\.-]+)( \(.*\)?$)?") + re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> (.+/[\w_\.-]+)( \(.*\)?$)?") _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } @@ -110,7 +110,6 @@ class Remote(LazyMixin, Iterable): = means the head was up to date ( and not moved ) ' ' means a fast-forward """ - line = line.strip() match = cls.re_fetch_result.match(line) if match is None: raise ValueError("Failed to parse line: %r" % line) -- cgit v1.2.1 From b1f32e231d391f8e6051957ad947d3659c196b2b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 27 Oct 2009 22:06:18 +0100 Subject: Added remote stale_refs property including test, tested new remote branch handling and deletion of stale remote branches --- lib/git/remote.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index d4ca9eb3..0c779f85 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -213,7 +213,7 @@ class Remote(LazyMixin, Iterable): def refs(self): """ Returns - List of RemoteRef objects + IterableList of RemoteReference objects """ out_refs = IterableList(RemoteReference._id_attribute_) for ref in RemoteReference.list_items(self.repo): @@ -223,6 +223,26 @@ class Remote(LazyMixin, Iterable): # END for each ref assert out_refs, "Remote %s did not have any references" % self.name return out_refs + + @property + def stale_refs(self): + """ + Returns + IterableList RemoteReference objects that do not have a corresponding + head in the remote reference anymore as they have been deleted on the + remote side, but are still available locally. + """ + out_refs = IterableList(RemoteReference._id_attribute_) + for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: + # expecting + # * [would prune] origin/new_branch + token = " * [would prune] " + if not line.startswith(token): + raise ValueError("Could not parse git-remote prune result: %r" % line) + fqhn = "%s/%s" % (RemoteReference._common_path_default,line.replace(token, "")) + out_refs.append(RemoteReference(self.repo, fqhn)) + # END for each line + return out_refs @classmethod def create(cls, repo, name, url, **kwargs): -- cgit v1.2.1 From 29c20c147b489d873fb988157a37bcf96f96ab45 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 27 Oct 2009 22:36:41 +0100 Subject: Added special cases to test that shows we cannot yet: handle the FETCH_HEAD case and handle tags System needs to be adjusted to take the FETCH_HEAD info into account to cover the tags case --- lib/git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 0c779f85..7f674d73 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -68,7 +68,7 @@ class Remote(LazyMixin, Iterable): BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] # %c %-*s %-*s -> %s (%s) - re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> (.+/[\w_\.-]+)( \(.*\)?$)?") + re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } -- cgit v1.2.1 From 2f8e6f7ab1e6dbd95c268ba0fc827abc62009013 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 27 Oct 2009 23:12:10 +0100 Subject: Implemented handling of FETCH_HEAD and tags, some test cases still missing dealing with deletion and movements of remote tags ( which in fact is discouraged, but we should be able to deal with it, shouldnt we ;) --- lib/git/remote.py | 87 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 22 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 7f674d73..dde3be4c 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -9,7 +9,7 @@ Module implementing a remote object allowing easy access to git remotes from git.utils import LazyMixin, Iterable, IterableList from objects import Commit -from refs import Reference, RemoteReference +from refs import Reference, RemoteReference, SymbolicReference, TagReference import re import os @@ -57,13 +57,16 @@ class Remote(LazyMixin, Iterable): Carries information about the results of a fetch operation:: info = remote.fetch()[0] - info.remote_ref # Symbolic Reference or RemoteReference to the changed remote head or FETCH_HEAD - info.flags # additional flags to be & with enumeration members, i.e. info.flags & info.REJECTED + info.ref # Symbolic Reference or RemoteReference to the changed + # remote head or FETCH_HEAD + info.flags # additional flags to be & with enumeration members, + # i.e. info.flags & info.REJECTED + # is 0 if ref is SymbolicReference info.note # additional notes given by git-fetch intended for the user - info.commit_before_forced_update # if info.flags & info.FORCED_UPDATE, field is set to the - # previous location of remote_ref, otherwise None + info.commit_before_forced_update # if info.flags & info.FORCED_UPDATE, + # field is set to the previous location of ref, otherwise None """ - __slots__ = ('remote_ref','commit_before_forced_update', 'flags', 'note') + __slots__ = ('ref','commit_before_forced_update', 'flags', 'note') BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] @@ -73,11 +76,11 @@ class Remote(LazyMixin, Iterable): _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } - def __init__(self, remote_ref, flags, note = '', old_commit = None): + def __init__(self, ref, flags, note = '', old_commit = None): """ Initialize a new instance """ - self.remote_ref = remote_ref + self.ref = ref self.flags = flags self.note = note self.commit_before_forced_update = old_commit @@ -91,10 +94,10 @@ class Remote(LazyMixin, Iterable): Returns Name of our remote ref """ - return self.remote_ref.name + return self.ref.name @classmethod - def _from_line(cls, repo, line): + def _from_line(cls, repo, line, fetch_line): """ Parse information from the given line as returned by git-fetch -v and return a new FetchInfo object representing this information. @@ -109,13 +112,43 @@ class Remote(LazyMixin, Iterable): * means birth of new branch or tag = means the head was up to date ( and not moved ) ' ' means a fast-forward + + fetch line is the corresponding line from FETCH_HEAD, like + acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo """ match = cls.re_fetch_result.match(line) if match is None: raise ValueError("Failed to parse line: %r" % line) + + # parse lines control_character, operation, local_remote_ref, remote_local_ref, note = match.groups() + try: + new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t") + ref_type_name, fetch_note = fetch_note.split(' ', 1) + except ValueError: # unpack error + raise ValueError("Failed to parse FETCH__HEAD line: %r" % fetch_line) + + # handle FETCH_HEAD and figure out ref type + # If we do not specify a target branch like master:refs/remotes/origin/master, + # the fetch result is stored in FETCH_HEAD which destroys the rule we usually + # have. In that case we use a symbolic reference which is detached + ref_type = None + if remote_local_ref == "FETCH_HEAD": + ref_type = SymbolicReference + elif ref_type_name == "branch": + ref_type = RemoteReference + elif ref_type_name == "tag": + ref_type = TagReference + else: + raise TypeError("Cannot handle reference type: %r" % ref_type_name) + + # create ref instance + if ref_type is SymbolicReference: + remote_local_ref = ref_type(repo, "FETCH_HEAD") + else: + remote_local_ref = Reference.from_path(repo, os.path.join(ref_type._common_path_default, remote_local_ref.strip())) + # END create ref instance - remote_local_ref = Reference.from_path(repo, os.path.join(RemoteReference._common_path_default, remote_local_ref.strip())) note = ( note and note.strip() ) or '' # parse flags from control_character @@ -126,17 +159,19 @@ class Remote(LazyMixin, Iterable): raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) # END control char exception hanlding - # parse operation string for more info + # parse operation string for more info - makes no sense for symbolic refs old_commit = None - if 'rejected' in operation: - flags |= cls.REJECTED - if 'new tag' in operation: - flags |= cls.NEW_TAG - if 'new branch' in operation: - flags |= cls.NEW_BRANCH - if '...' in operation: - old_commit = Commit(repo, operation.split('...')[0]) - # END handle refspec + if isinstance(remote_local_ref, Reference): + if 'rejected' in operation: + flags |= cls.REJECTED + if 'new tag' in operation: + flags |= cls.NEW_TAG + if 'new branch' in operation: + flags |= cls.NEW_BRANCH + if '...' in operation: + old_commit = Commit(repo, operation.split('...')[0]) + # END handle refspec + # END reference flag handling return cls(remote_local_ref, flags, note, old_commit) @@ -316,7 +351,15 @@ class Remote(LazyMixin, Iterable): # skip first line as it is some remote info we are not interested in print stderr output = IterableList('name') - output.extend(self.FetchInfo._from_line(self.repo, line) for line in stderr.splitlines()[1:]) + err_info = stderr.splitlines()[1:] + + # read head information + fp = open(os.path.join(self.repo.path, 'FETCH_HEAD'),'r') + fetch_head_info = fp.readlines() + fp.close() + + output.extend(self.FetchInfo._from_line(self.repo, err_line, fetch_line) + for err_line,fetch_line in zip(err_info, fetch_head_info)) return output def fetch(self, refspec=None, **kwargs): -- cgit v1.2.1 From 87afd252bd11026b6ba3db8525f949cfb62c90fc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 28 Oct 2009 10:58:24 +0100 Subject: tag handling tests finished, unfortunately there is not yet a rejected case, but it will assuambly follow with the push tests --- lib/git/remote.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index dde3be4c..02a955b0 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -96,6 +96,14 @@ class Remote(LazyMixin, Iterable): """ return self.ref.name + @property + def commit(self): + """ + Returns + Commit of our remote ref + """ + return self.ref.commit + @classmethod def _from_line(cls, repo, line, fetch_line): """ -- cgit v1.2.1 From 146a6fe18da94e12aa46ec74582db640e3bbb3a9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 28 Oct 2009 12:00:58 +0100 Subject: IterableList: added support for prefix allowing remote.refs.master constructs, previously it was remote.refs['%s/master'%remote] Added first simple test for push support, which shows that much more work is needed on that side to allow just-in-time progress information --- lib/git/remote.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 02a955b0..47743913 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -256,9 +256,11 @@ class Remote(LazyMixin, Iterable): def refs(self): """ Returns - IterableList of RemoteReference objects + IterableList of RemoteReference objects. It is prefixed, allowing + you to omit the remote path portion, i.e.:: + remote.refs.master # yields RemoteReference('/refs/remotes/origin/master') """ - out_refs = IterableList(RemoteReference._id_attribute_) + out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) for ref in RemoteReference.list_items(self.repo): if ref.remote_name == self.name: out_refs.append(ref) @@ -274,8 +276,11 @@ class Remote(LazyMixin, Iterable): IterableList RemoteReference objects that do not have a corresponding head in the remote reference anymore as they have been deleted on the remote side, but are still available locally. + + The IterableList is prefixed, hence the 'origin' must be omitted. See + 'refs' property for an example. """ - out_refs = IterableList(RemoteReference._id_attribute_) + out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: # expecting # * [would prune] origin/new_branch @@ -357,7 +362,6 @@ class Remote(LazyMixin, Iterable): def _get_fetch_info_from_stderr(self, stderr): # skip first line as it is some remote info we are not interested in - print stderr output = IterableList('name') err_info = stderr.splitlines()[1:] @@ -426,7 +430,12 @@ class Remote(LazyMixin, Iterable): Returns self """ - self.repo.git.push(self, refspec, **kwargs) + proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, **kwargs) + print "stdout"*10 + print proc.stdout.read() + print "stderr"*10 + print proc.stderr.read() + proc.wait() return self @property -- cgit v1.2.1 From dc518251eb64c3ef90502697a7e08abe3f8310b2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 28 Oct 2009 12:03:18 +0100 Subject: FetchInfo class is not a subclass of Remote class anymore, as more classes are to be added it cluttered up the view and made things more complex as well --- lib/git/remote.py | 274 +++++++++++++++++++++++++++--------------------------- 1 file changed, 137 insertions(+), 137 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 47743913..ace5128a 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -37,6 +37,138 @@ class _SectionConstraint(object): as first argument""" return getattr(self._config, method)(self._section_name, *args) + +class FetchInfo(object): + """ + Carries information about the results of a fetch operation:: + + info = remote.fetch()[0] + info.ref # Symbolic Reference or RemoteReference to the changed + # remote head or FETCH_HEAD + info.flags # additional flags to be & with enumeration members, + # i.e. info.flags & info.REJECTED + # is 0 if ref is SymbolicReference + info.note # additional notes given by git-fetch intended for the user + info.commit_before_forced_update # if info.flags & info.FORCED_UPDATE, + # field is set to the previous location of ref, otherwise None + """ + __slots__ = ('ref','commit_before_forced_update', 'flags', 'note') + + BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ + TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] + # %c %-*s %-*s -> %s (%s) + re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") + + _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, + '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } + + def __init__(self, ref, flags, note = '', old_commit = None): + """ + Initialize a new instance + """ + self.ref = ref + self.flags = flags + self.note = note + self.commit_before_forced_update = old_commit + + def __str__(self): + return self.name + + @property + def name(self): + """ + Returns + Name of our remote ref + """ + return self.ref.name + + @property + def commit(self): + """ + Returns + Commit of our remote ref + """ + return self.ref.commit + + @classmethod + def _from_line(cls, repo, line, fetch_line): + """ + Parse information from the given line as returned by git-fetch -v + and return a new FetchInfo object representing this information. + + We can handle a line as follows + "%c %-*s %-*s -> %s%s" + + Where c is either ' ', !, +, -, *, or = + ! means error + + means success forcing update + - means a tag was updated + * means birth of new branch or tag + = means the head was up to date ( and not moved ) + ' ' means a fast-forward + + fetch line is the corresponding line from FETCH_HEAD, like + acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo + """ + match = cls.re_fetch_result.match(line) + if match is None: + raise ValueError("Failed to parse line: %r" % line) + + # parse lines + control_character, operation, local_remote_ref, remote_local_ref, note = match.groups() + try: + new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t") + ref_type_name, fetch_note = fetch_note.split(' ', 1) + except ValueError: # unpack error + raise ValueError("Failed to parse FETCH__HEAD line: %r" % fetch_line) + + # handle FETCH_HEAD and figure out ref type + # If we do not specify a target branch like master:refs/remotes/origin/master, + # the fetch result is stored in FETCH_HEAD which destroys the rule we usually + # have. In that case we use a symbolic reference which is detached + ref_type = None + if remote_local_ref == "FETCH_HEAD": + ref_type = SymbolicReference + elif ref_type_name == "branch": + ref_type = RemoteReference + elif ref_type_name == "tag": + ref_type = TagReference + else: + raise TypeError("Cannot handle reference type: %r" % ref_type_name) + + # create ref instance + if ref_type is SymbolicReference: + remote_local_ref = ref_type(repo, "FETCH_HEAD") + else: + remote_local_ref = Reference.from_path(repo, os.path.join(ref_type._common_path_default, remote_local_ref.strip())) + # END create ref instance + + note = ( note and note.strip() ) or '' + + # parse flags from control_character + flags = 0 + try: + flags |= cls._flag_map[control_character] + except KeyError: + raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) + # END control char exception hanlding + + # parse operation string for more info - makes no sense for symbolic refs + old_commit = None + if isinstance(remote_local_ref, Reference): + if 'rejected' in operation: + flags |= cls.REJECTED + if 'new tag' in operation: + flags |= cls.NEW_TAG + if 'new branch' in operation: + flags |= cls.NEW_BRANCH + if '...' in operation: + old_commit = Commit(repo, operation.split('...')[0]) + # END handle refspec + # END reference flag handling + + return cls(remote_local_ref, flags, note, old_commit) + class Remote(LazyMixin, Iterable): """ @@ -52,140 +184,6 @@ class Remote(LazyMixin, Iterable): __slots__ = ( "repo", "name", "_config_reader" ) _id_attribute_ = "name" - class FetchInfo(object): - """ - Carries information about the results of a fetch operation:: - - info = remote.fetch()[0] - info.ref # Symbolic Reference or RemoteReference to the changed - # remote head or FETCH_HEAD - info.flags # additional flags to be & with enumeration members, - # i.e. info.flags & info.REJECTED - # is 0 if ref is SymbolicReference - info.note # additional notes given by git-fetch intended for the user - info.commit_before_forced_update # if info.flags & info.FORCED_UPDATE, - # field is set to the previous location of ref, otherwise None - """ - __slots__ = ('ref','commit_before_forced_update', 'flags', 'note') - - BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ - TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] - # %c %-*s %-*s -> %s (%s) - re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") - - _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, - '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } - - def __init__(self, ref, flags, note = '', old_commit = None): - """ - Initialize a new instance - """ - self.ref = ref - self.flags = flags - self.note = note - self.commit_before_forced_update = old_commit - - def __str__(self): - return self.name - - @property - def name(self): - """ - Returns - Name of our remote ref - """ - return self.ref.name - - @property - def commit(self): - """ - Returns - Commit of our remote ref - """ - return self.ref.commit - - @classmethod - def _from_line(cls, repo, line, fetch_line): - """ - Parse information from the given line as returned by git-fetch -v - and return a new FetchInfo object representing this information. - - We can handle a line as follows - "%c %-*s %-*s -> %s%s" - - Where c is either ' ', !, +, -, *, or = - ! means error - + means success forcing update - - means a tag was updated - * means birth of new branch or tag - = means the head was up to date ( and not moved ) - ' ' means a fast-forward - - fetch line is the corresponding line from FETCH_HEAD, like - acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo - """ - match = cls.re_fetch_result.match(line) - if match is None: - raise ValueError("Failed to parse line: %r" % line) - - # parse lines - control_character, operation, local_remote_ref, remote_local_ref, note = match.groups() - try: - new_hex_sha, fetch_operation, fetch_note = fetch_line.split("\t") - ref_type_name, fetch_note = fetch_note.split(' ', 1) - except ValueError: # unpack error - raise ValueError("Failed to parse FETCH__HEAD line: %r" % fetch_line) - - # handle FETCH_HEAD and figure out ref type - # If we do not specify a target branch like master:refs/remotes/origin/master, - # the fetch result is stored in FETCH_HEAD which destroys the rule we usually - # have. In that case we use a symbolic reference which is detached - ref_type = None - if remote_local_ref == "FETCH_HEAD": - ref_type = SymbolicReference - elif ref_type_name == "branch": - ref_type = RemoteReference - elif ref_type_name == "tag": - ref_type = TagReference - else: - raise TypeError("Cannot handle reference type: %r" % ref_type_name) - - # create ref instance - if ref_type is SymbolicReference: - remote_local_ref = ref_type(repo, "FETCH_HEAD") - else: - remote_local_ref = Reference.from_path(repo, os.path.join(ref_type._common_path_default, remote_local_ref.strip())) - # END create ref instance - - note = ( note and note.strip() ) or '' - - # parse flags from control_character - flags = 0 - try: - flags |= cls._flag_map[control_character] - except KeyError: - raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) - # END control char exception hanlding - - # parse operation string for more info - makes no sense for symbolic refs - old_commit = None - if isinstance(remote_local_ref, Reference): - if 'rejected' in operation: - flags |= cls.REJECTED - if 'new tag' in operation: - flags |= cls.NEW_TAG - if 'new branch' in operation: - flags |= cls.NEW_BRANCH - if '...' in operation: - old_commit = Commit(repo, operation.split('...')[0]) - # END handle refspec - # END reference flag handling - - return cls(remote_local_ref, flags, note, old_commit) - - # END FetchInfo definition - - def __init__(self, repo, name): """ Initialize a remote instance @@ -370,7 +368,7 @@ class Remote(LazyMixin, Iterable): fetch_head_info = fp.readlines() fp.close() - output.extend(self.FetchInfo._from_line(self.repo, err_line, fetch_line) + output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) for err_line,fetch_line in zip(err_info, fetch_head_info)) return output @@ -428,8 +426,10 @@ class Remote(LazyMixin, Iterable): Additional arguments to be passed to git-push Returns - self - """ + IterableList(PushInfo, ...) iterable list of PushInfo instances, each + one informing about an individual head which had been updated on the remote + side + """ proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, **kwargs) print "stdout"*10 print proc.stdout.read() -- cgit v1.2.1 From 4712c619ed6a2ce54b781fe404fedc269b77e5dd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 28 Oct 2009 15:15:14 +0100 Subject: Fixed bug when listing remotes - it was based on references which is incorrect as it cannot always work --- lib/git/remote.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index ace5128a..562a5082 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -238,16 +238,9 @@ class Remote(LazyMixin, Iterable): Returns Iterator yielding Remote objects of the given repository """ - # parse them using refs, as their query can be faster as it is - # purely based on the file system seen_remotes = set() - for ref in RemoteReference.iter_items(repo): - remote_name = ref.remote_name - if remote_name in seen_remotes: - continue - # END if remote done already - seen_remotes.add(remote_name) - yield Remote(repo, remote_name) + for name in repo.git.remote().splitlines(): + yield Remote(repo, name) # END for each ref @property -- cgit v1.2.1 From 461fd06e1f2d91dfbe47168b53815086212862e4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 28 Oct 2009 23:35:41 +0100 Subject: Added frame for push testing and push implemenation --- lib/git/remote.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 13 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 562a5082..5a8c8604 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -38,9 +38,87 @@ class _SectionConstraint(object): return getattr(self._config, method)(self._section_name, *args) +class PushProgress(object): + """ + Handler providing an interface to parse progress information emitted by git-push + and to dispatch callbacks allowing subclasses to react to the progress. + """ + BEGIN, END, COUNTING, COMPRESSING, WRITING = [ 1 << x for x in range(1,6) ] + STAGE_MASK = BEGIN|END + OP_MASK = COUNTING|COMPRESSING|WRITING + + __slots__ = "_cur_line" + + def _parse_progress_line(self, line): + """ + Parse progress information from the given line as retrieved by git-push + """ + self._cur_line = line + + def line_dropped(self, line): + """ + Called whenever a line could not be understood and was therefore dropped. + """ + + def update(self, op_code, cur_count, max_count=None): + """ + Called whenever the progress changes + + ``op_code`` + Integer allowing to be compared against Operation IDs and stage IDs. + + Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation + ID as well as END. It may be that BEGIN and END are set at once in case only + one progress message was emitted due to the speed of the operation. + Between BEGIN and END, none of these flags will be set + + Operation IDs are all held within the OP_MASK. Only one Operation ID will + be active per call. + + ``cur_count`` + Current absolute count of items + + ``max_count`` + The maximum count of items we expect. It may be None in case there is + no maximum number of items or if it is (yet) unknown. + + You may read the contents of the current line in self._cur_line + """ + + +class PushInfo(object): + """ + Carries information about the result of a push operation of a single head:: + todo + + """ + __slots__ = ('local_ref', 'remote_ref') + + NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ + FORCED_UPDATE, FAST_FORWARD, ERROR = [ 1 << x for x in range(1,9) ] + + _flag_map = { 'X' : NO_MATCH, '-' : DELETED, '*' : 0, + '+' : FORCED_UPDATE, ' ' : FAST_FORWARD } + + def __init__(self, local_ref, remote_ref): + """ + Initialize a new instance + """ + self.local_ref = local_ref + self.remote_ref = remote_ref + + @classmethod + def _from_line(cls, repo, line): + """ + Create a new PushInfo instance as parsed from line which is expected to be like + c refs/heads/master:refs/heads/master 05d2687..1d0568e + """ + raise NotImplementedError("todo") + + class FetchInfo(object): """ - Carries information about the results of a fetch operation:: + Carries information about the results of a fetch operation of a single head:: info = remote.fetch()[0] info.ref # Symbolic Reference or RemoteReference to the changed @@ -54,13 +132,13 @@ class FetchInfo(object): """ __slots__ = ('ref','commit_before_forced_update', 'flags', 'note') - BRANCH_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ - TAG_UPDATE, NEW_BRANCH, ERROR = [ 1 << x for x in range(1,9) ] + HEAD_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ + TAG_UPDATE, NEW_HEAD, ERROR = [ 1 << x for x in range(1,9) ] # %c %-*s %-*s -> %s (%s) re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") _flag_map = { '!' : ERROR, '+' : FORCED_UPDATE, '-' : TAG_UPDATE, '*' : 0, - '=' : BRANCH_UPTODATE, ' ' : FAST_FORWARD } + '=' : HEAD_UPTODATE, ' ' : FAST_FORWARD } def __init__(self, ref, flags, note = '', old_commit = None): """ @@ -161,7 +239,7 @@ class FetchInfo(object): if 'new tag' in operation: flags |= cls.NEW_TAG if 'new branch' in operation: - flags |= cls.NEW_BRANCH + flags |= cls.NEW_HEAD if '...' in operation: old_commit = Commit(repo, operation.split('...')[0]) # END handle refspec @@ -365,6 +443,19 @@ class Remote(LazyMixin, Iterable): for err_line,fetch_line in zip(err_info, fetch_head_info)) return output + def _get_push_info(self, proc, progress): + # read progress information from stderr + # we hope stdout can hold all the data, it should ... + for line in proc.stderr.readline(): + progress._parse_progress_line(line) + # END for each progress line + + output = IterableList('name') + output.extend(PushInfo._from_line(self.repo, line) for line in proc.stdout.readlines()) + proc.wait() + return output + + def fetch(self, refspec=None, **kwargs): """ Fetch the latest changes for this remote @@ -405,16 +496,21 @@ class Remote(LazyMixin, Iterable): Returns Please see 'fetch' method """ - status, stdout, stderr = self.repo.git.pull(self, refspec, v=True, with_extended_output=True, **kwargs) + status, stdout, stderr = self.repo.git.pull(self, refspec, with_extended_output=True, v=True, **kwargs) return self._get_fetch_info_from_stderr(stderr) - def push(self, refspec=None, **kwargs): + def push(self, refspec=None, progress=None, **kwargs): """ Push changes from source branch in refspec to target branch in refspec. ``refspec`` see 'fetch' method + ``progress`` + Instance of type PushProgress allowing the caller to receive + progress information until the method returns. + If None, progress information will be discarded + ``**kwargs`` Additional arguments to be passed to git-push @@ -424,12 +520,7 @@ class Remote(LazyMixin, Iterable): side """ proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, **kwargs) - print "stdout"*10 - print proc.stdout.read() - print "stderr"*10 - print proc.stderr.read() - proc.wait() - return self + return self._get_push_info(proc, progress or PushProgress()) @property def config_reader(self): -- cgit v1.2.1 From b2ccae0d7fca3a99fc6a3f85f554d162a3fdc916 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 2 Nov 2009 22:39:54 +0100 Subject: Implemented PushProgress and PushInfo class including basic test cases. Now many more test-cases need to be added to be sure we can truly deal with everything git throws at us --- lib/git/remote.py | 151 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 134 insertions(+), 17 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 5a8c8604..a19428b2 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -43,24 +43,74 @@ class PushProgress(object): Handler providing an interface to parse progress information emitted by git-push and to dispatch callbacks allowing subclasses to react to the progress. """ - BEGIN, END, COUNTING, COMPRESSING, WRITING = [ 1 << x for x in range(1,6) ] + BEGIN, END, COUNTING, COMPRESSING, WRITING = [ 1 << x for x in range(5) ] STAGE_MASK = BEGIN|END OP_MASK = COUNTING|COMPRESSING|WRITING - __slots__ = "_cur_line" + __slots__ = ("_cur_line", "_seen_ops") + re_op_absolute = re.compile("([\w\s]+):\s+()(\d+)()(, done\.)?\s*") + re_op_relative = re.compile("([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(,.* done\.)?$") + + def __init__(self): + self._seen_ops = list() def _parse_progress_line(self, line): """ Parse progress information from the given line as retrieved by git-push """ + # handle + # Counting objects: 4, done. + # Compressing objects: 50% (1/2) \rCompressing objects: 100% (2/2) \rCompressing objects: 100% (2/2), done. self._cur_line = line + sub_lines = line.split('\r') + for sline in sub_lines: + sline = sline.rstrip() + + cur_count, max_count = None, None + match = self.re_op_relative.match(sline) + if match is None: + match = self.re_op_absolute.match(sline) + + if not match: + self.line_dropped(sline) + continue + # END could not get match + + op_code = 0 + op_name, percent, cur_count, max_count, done = match.groups() + # get operation id + if op_name == "Counting objects": + op_code |= self.COUNTING + elif op_name == "Compressing objects": + op_code |= self.COMPRESSING + elif op_name == "Writing objects": + op_code |= self.WRITING + else: + raise ValueError("Operation name %r unknown" % op_name) + + # figure out stage + if op_code not in self._seen_ops: + self._seen_ops.append(op_code) + op_code |= self.BEGIN + # END begin opcode + + message = '' + if done is not None and 'done.' in done: + op_code |= self.END + message = done.replace( ", done.", "")[2:] + # END end flag handling + + self.update(op_code, cur_count, max_count, message) + + # END for each sub line def line_dropped(self, line): """ Called whenever a line could not be understood and was therefore dropped. """ + pass - def update(self, op_code, cur_count, max_count=None): + def update(self, op_code, cur_count, max_count=None, message=''): """ Called whenever the progress changes @@ -81,39 +131,106 @@ class PushProgress(object): ``max_count`` The maximum count of items we expect. It may be None in case there is no maximum number of items or if it is (yet) unknown. - + + ``message`` + In case of the 'WRITING' operation, it contains the amount of bytes + transferred. It may possibly be used for other purposes as well. You may read the contents of the current line in self._cur_line """ + pass class PushInfo(object): """ Carries information about the result of a push operation of a single head:: - todo - + info = remote.push()[0] + info.flags # bitflags providing more information about the result + info.local_ref # Reference pointing to the local reference that was pushed + info.remote_ref_string # path to the remote reference located on the remote side + info.remote_ref # Remote Reference on the local side corresponding to + # the remote_ref_string. It can be a TagReference as well. + info.old_commit # commit at which the remote_ref was standing before we pushed + # it to local_ref.commit. Will be None if an error was indicated """ - __slots__ = ('local_ref', 'remote_ref') + __slots__ = ('local_ref', 'remote_ref_string', 'flags', 'old_commit', '_remote') NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ - FORCED_UPDATE, FAST_FORWARD, ERROR = [ 1 << x for x in range(1,9) ] + FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [ 1 << x for x in range(9) ] _flag_map = { 'X' : NO_MATCH, '-' : DELETED, '*' : 0, - '+' : FORCED_UPDATE, ' ' : FAST_FORWARD } + '+' : FORCED_UPDATE, ' ' : FAST_FORWARD, + '=' : UP_TO_DATE, '!' : ERROR } - def __init__(self, local_ref, remote_ref): + def __init__(self, flags, local_ref, remote_ref_string, remote, old_commit=None): """ Initialize a new instance """ + self.flags = flags self.local_ref = local_ref - self.remote_ref = remote_ref + self.remote_ref_string = remote_ref_string + self._remote = remote + self.old_commit = old_commit + + @property + def remote_ref(self): + """ + Returns + Remote Reference or TagReference in the local repository corresponding + to the remote_ref_string kept in this instance. + """ + # translate heads to a local remote, tags stay as they are + if self.remote_ref_string.startswith("refs/tags"): + return TagReference(self._remote.repo, self.remote_ref_string) + elif self.remote_ref_string.startswith("refs/heads"): + remote_ref = Reference(self._remote.repo, self.remote_ref_string) + return RemoteReference(self._remote.repo, "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name)) + else: + raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string) + # END @classmethod - def _from_line(cls, repo, line): + def _from_line(cls, remote, line): """ Create a new PushInfo instance as parsed from line which is expected to be like c refs/heads/master:refs/heads/master 05d2687..1d0568e """ - raise NotImplementedError("todo") + control_character, from_to, summary = line.split('\t', 3) + flags = 0 + + # control character handling + try: + flags |= cls._flag_map[ control_character ] + except KeyError: + raise ValueError("Control Character %r unknown as parsed from line %r" % (control_character, line)) + # END handle control character + + # from_to handling + from_ref_string, to_ref_string = from_to.split(':') + from_ref = Reference.from_path(remote.repo, from_ref_string) + + # commit handling, could be message or commit info + old_commit = None + if summary.startswith('['): + if "[rejected]" in summary: + flags |= cls.REJECTED + elif "[remote rejected]" in summary: + flags |= cls.REMOTE_REJECTED + elif "[remote failure]" in summary: + flags |= cls.REMOTE_FAILURE + elif "[no match]" in summary: + flags |= cls.ERROR + # uptodate encoded in control character + else: + # fast-forward or forced update - was encoded in control character, + # but we parse the old and new commit + split_token = "..." + if control_character == " ": + split_token = ".." + old_sha, new_sha = summary.split(' ')[0].split(split_token) + old_commit = Commit(remote.repo, old_sha) + # END message handling + + return PushInfo(flags, from_ref, to_ref_string, remote, old_commit) class FetchInfo(object): @@ -133,7 +250,7 @@ class FetchInfo(object): __slots__ = ('ref','commit_before_forced_update', 'flags', 'note') HEAD_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ - TAG_UPDATE, NEW_HEAD, ERROR = [ 1 << x for x in range(1,9) ] + TAG_UPDATE, NEW_HEAD, ERROR = [ 1 << x for x in range(8) ] # %c %-*s %-*s -> %s (%s) re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") @@ -446,12 +563,12 @@ class Remote(LazyMixin, Iterable): def _get_push_info(self, proc, progress): # read progress information from stderr # we hope stdout can hold all the data, it should ... - for line in proc.stderr.readline(): - progress._parse_progress_line(line) + for line in proc.stderr.readlines(): + progress._parse_progress_line(line.rstrip()) # END for each progress line output = IterableList('name') - output.extend(PushInfo._from_line(self.repo, line) for line in proc.stdout.readlines()) + output.extend(PushInfo._from_line(self, line) for line in proc.stdout.readlines()) proc.wait() return output -- cgit v1.2.1 From e70f3218e910d2b3dcb8a5ab40c65b6bd7a8e9a8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 3 Nov 2009 14:04:32 +0100 Subject: Intermediate commit with a few added and improved tests as well as many fixes --- lib/git/remote.py | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index a19428b2..482df233 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -7,9 +7,11 @@ Module implementing a remote object allowing easy access to git remotes """ +from errors import GitCommandError from git.utils import LazyMixin, Iterable, IterableList from objects import Commit from refs import Reference, RemoteReference, SymbolicReference, TagReference + import re import os @@ -146,6 +148,7 @@ class PushInfo(object): info = remote.push()[0] info.flags # bitflags providing more information about the result info.local_ref # Reference pointing to the local reference that was pushed + # It is None if the ref was deleted. info.remote_ref_string # path to the remote reference located on the remote side info.remote_ref # Remote Reference on the local side corresponding to # the remote_ref_string. It can be a TagReference as well. @@ -154,8 +157,8 @@ class PushInfo(object): """ __slots__ = ('local_ref', 'remote_ref_string', 'flags', 'old_commit', '_remote') - NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ - FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [ 1 << x for x in range(9) ] + NEW_TAG, NEW_HEAD, NO_MATCH, REJECTED, REMOTE_REJECTED, REMOTE_FAILURE, DELETED, \ + FORCED_UPDATE, FAST_FORWARD, UP_TO_DATE, ERROR = [ 1 << x for x in range(11) ] _flag_map = { 'X' : NO_MATCH, '-' : DELETED, '*' : 0, '+' : FORCED_UPDATE, ' ' : FAST_FORWARD, @@ -194,6 +197,7 @@ class PushInfo(object): Create a new PushInfo instance as parsed from line which is expected to be like c refs/heads/master:refs/heads/master 05d2687..1d0568e """ + print line control_character, from_to, summary = line.split('\t', 3) flags = 0 @@ -206,7 +210,10 @@ class PushInfo(object): # from_to handling from_ref_string, to_ref_string = from_to.split(':') - from_ref = Reference.from_path(remote.repo, from_ref_string) + if flags & cls.DELETED: + from_ref = None + else: + from_ref = Reference.from_path(remote.repo, from_ref_string) # commit handling, could be message or commit info old_commit = None @@ -219,6 +226,10 @@ class PushInfo(object): flags |= cls.REMOTE_FAILURE elif "[no match]" in summary: flags |= cls.ERROR + elif "[new tag]" in summary: + flags |= cls.NEW_TAG + elif "[new branch]" in summary: + flags |= cls.NEW_HEAD # uptodate encoded in control character else: # fast-forward or forced update - was encoded in control character, @@ -249,8 +260,9 @@ class FetchInfo(object): """ __slots__ = ('ref','commit_before_forced_update', 'flags', 'note') - HEAD_UPTODATE, REJECTED, FORCED_UPDATE, FAST_FORWARD, NEW_TAG, \ - TAG_UPDATE, NEW_HEAD, ERROR = [ 1 << x for x in range(8) ] + NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ + FAST_FORWARD, ERROR = [ 1 << x for x in range(8) ] + # %c %-*s %-*s -> %s (%s) re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\.-]+)( \(.*\)?$)?") @@ -568,8 +580,20 @@ class Remote(LazyMixin, Iterable): # END for each progress line output = IterableList('name') - output.extend(PushInfo._from_line(self, line) for line in proc.stdout.readlines()) - proc.wait() + for line in proc.stdout.readlines(): + try: + output.append(PushInfo._from_line(self, line)) + except ValueError: + # if an error happens, additional info is given which we cannot parse + pass + # END exception handling + # END for each line + try: + proc.wait() + except GitCommandError: + # if a push has rejected items, the command has non-zero return status + pass + # END exception handling return output @@ -634,7 +658,11 @@ class Remote(LazyMixin, Iterable): Returns IterableList(PushInfo, ...) iterable list of PushInfo instances, each one informing about an individual head which had been updated on the remote - side + side. + If the push contains rejected heads, these will have the PushInfo.ERROR bit set + in their flags. + If the operation fails completely, the length of the returned IterableList will + be null. """ proc = self.repo.git.push(self, refspec, porcelain=True, as_process=True, **kwargs) return self._get_push_info(proc, progress or PushProgress()) -- cgit v1.2.1 From ec3d91644561ef59ecdde59ddced38660923e916 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 3 Nov 2009 14:28:22 +0100 Subject: Finished all push tests I could think of so far. More error cases should be studied, but they would be hard to 'produce' --- lib/git/remote.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'lib/git/remote.py') diff --git a/lib/git/remote.py b/lib/git/remote.py index 482df233..1b9c5360 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -197,7 +197,6 @@ class PushInfo(object): Create a new PushInfo instance as parsed from line which is expected to be like c refs/heads/master:refs/heads/master 05d2687..1d0568e """ - print line control_character, from_to, summary = line.split('\t', 3) flags = 0 @@ -619,6 +618,10 @@ class Remote(LazyMixin, Iterable): Returns IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed information about the fetch results + + Note + As fetch does not provide progress information to non-ttys, we cannot make + it available here unfortunately as in the 'push' method. """ status, stdout, stderr = self.repo.git.fetch(self, refspec, with_extended_output=True, v=True, **kwargs) return self._get_fetch_info_from_stderr(stderr) -- cgit v1.2.1