diff options
author | Sebastian Thiel <byronimo@gmail.com> | 2011-06-07 13:38:48 +0200 |
---|---|---|
committer | Sebastian Thiel <byronimo@gmail.com> | 2011-06-07 14:07:59 +0200 |
commit | 9bf3fdec93fe427bb5f0bd39c986a4e977969f41 (patch) | |
tree | 554683bc11a0818e1f0038be3fa20f8a0b694ed6 /git/db | |
parent | 09517bd78660ee3fbd6716c920c36b967f7a71cf (diff) | |
download | gitpython-9bf3fdec93fe427bb5f0bd39c986a4e977969f41.tar.gz |
First run in order to fix the remote handling. Cleaned up interfaces and figured out that the implementation really should be specific to the git command. This leaves the interface open for other implemntations which use a different way to provide feedback (as we do not make assumptions about the format of a feedback line)
Diffstat (limited to 'git/db')
-rw-r--r-- | git/db/cmd/base.py | 205 | ||||
-rw-r--r-- | git/db/interface.py | 66 | ||||
-rw-r--r-- | git/db/py/base.py | 5 |
3 files changed, 204 insertions, 72 deletions
diff --git a/git/db/cmd/base.py b/git/db/cmd/base.py index 393d9262..78adbc6e 100644 --- a/git/db/cmd/base.py +++ b/git/db/cmd/base.py @@ -14,18 +14,18 @@ from git.base import ( from git.util import ( bin_to_hex, hex_to_bin, - RemoteProgress, isfile, join_path, join, Actor, - IterableList + IterableList, ) -from git.db.interface import FetchInfo as GitdbFetchInfo -from git.db.interface import PushInfo as GitdbPushInfo from git.db.interface import ( + FetchInfo, + PushInfo, HighLevelRepository, - TransportDB + TransportDB, + RemoteProgress ) from git.cmd import Git from git.refs import ( @@ -41,8 +41,8 @@ import os import sys -__all__ = ('CmdTransportMixin', 'RemoteProgress', 'GitCommandMixin', - 'CmdObjectDBRMixin', 'CmdHighLevelRepository') +__all__ = ('CmdTransportMixin', 'GitCommandMixin', 'CmdPushInfo', 'CmdFetchInfo', + 'CmdRemoteProgress', 'CmdObjectDBRMixin', 'CmdHighLevelRepository') #{ Utilities @@ -115,13 +115,13 @@ def get_fetch_info_from_stderr(repo, proc, progress): assert len(fetch_info_lines) == len(fetch_head_info) - output.extend(FetchInfo._from_line(repo, err_line, fetch_line) + output.extend(CmdFetchInfo._from_line(repo, err_line, fetch_line) for err_line,fetch_line in zip(fetch_info_lines, fetch_head_info)) finalize_process(proc) return output -def get_push_info(repo, proc, progress): +def get_push_info(repo, remotename_or_url, proc, progress): # read progress information from stderr # we hope stdout can hold all the data, it should ... # read the lines manually as it will use carriage returns between the messages @@ -131,7 +131,7 @@ def get_push_info(repo, proc, progress): output = IterableList('name') for line in proc.stdout.readlines(): try: - output.append(PushInfo._from_line(repo, line)) + output.append(CmdPushInfo._from_line(repo, remotename_or_url, line)) except ValueError: # if an error happens, additional info is given which we cannot parse pass @@ -143,37 +143,119 @@ def get_push_info(repo, proc, progress): #} END utilities -class PushInfo(GitdbPushInfo): +class CmdRemoteProgress(RemoteProgress): """ - Carries information about the result of a push operation of a single head:: - - 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. - info.old_commit_binsha # binary sha at which the remote_ref was standing before we pushed - # it to local_ref.commit. Will be None if an error was indicated - info.summary # summary line providing human readable english text about the push - """ - __slots__ = ('local_ref', 'remote_ref_string', 'flags', 'old_commit_binsha', '_remote', 'summary') + A Remote progress implementation taking a user derived progress to call the + respective methods on. + """ + __slots__ = ("_seen_ops", '_progress') + re_op_absolute = re.compile("(remote: )?([\w\s]+):\s+()(\d+)()(.*)") + re_op_relative = re.compile("(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)") + + def __init__(self, progress_instance = None): + self._seen_ops = list() + if progress_instance is None: + progress_instance = RemoteProgress() + #END assure proper instance + self._progress = progress_instance + + def _parse_progress_line(self, line): + """Parse progress information from the given line as retrieved by git-push + or git-fetch + + Call the own update(), __call__() and line_dropped() methods according + to the parsed result. + + :return: list(line, ...) list of lines that could not be processed""" + # handle + # Counting objects: 4, done. + # Compressing objects: 50% (1/2) \rCompressing objects: 100% (2/2) \rCompressing objects: 100% (2/2), done. + sub_lines = line.split('\r') + failed_lines = list() + for sline in sub_lines: + # find esacpe characters and cut them away - regex will not work with + # them as they are non-ascii. As git might expect a tty, it will send them + last_valid_index = None + for i,c in enumerate(reversed(sline)): + if ord(c) < 32: + # its a slice index + last_valid_index = -i-1 + # END character was non-ascii + # END for each character in sline + if last_valid_index is not None: + sline = sline[:last_valid_index] + # END cut away invalid part + 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._progress.line_dropped(sline) + failed_lines.append(sline) + continue + # END could not get match + + op_code = 0 + remote, op_name, percent, cur_count, max_count, message = 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 + + if message is None: + message = '' + # END message handling + + message = message.strip() + done_token = ', done.' + if message.endswith(done_token): + op_code |= self.END + message = message[:-len(done_token)] + # END end message handling + + self._progress.update(op_code, cur_count, max_count, message, line) + self._progress(message, line) + # END for each sub line + return failed_lines + + +class CmdPushInfo(PushInfo): + """ + Pure Python implementation of a PushInfo interface + """ + __slots__ = ('local_ref', 'remote_ref_string', 'flags', 'old_commit_binsha', + '_remotename_or_url', 'repo', 'summary') - _flag_map = { 'X' : GitdbPushInfo.NO_MATCH, - '-' : GitdbPushInfo.DELETED, '*' : 0, - '+' : GitdbPushInfo.FORCED_UPDATE, - ' ' : GitdbPushInfo.FAST_FORWARD, - '=' : GitdbPushInfo.UP_TO_DATE, - '!' : GitdbPushInfo.ERROR } + _flag_map = { 'X' : PushInfo.NO_MATCH, + '-' : PushInfo.DELETED, '*' : 0, + '+' : PushInfo.FORCED_UPDATE, + ' ' : PushInfo.FAST_FORWARD, + '=' : PushInfo.UP_TO_DATE, + '!' : PushInfo.ERROR } - def __init__(self, flags, local_ref, remote_ref_string, remote, old_commit_binsha=None, + def __init__(self, flags, local_ref, remote_ref_string, repo, remotename_or_url, old_commit_binsha=None, summary=''): """ Initialize a new instance """ self.flags = flags self.local_ref = local_ref + self.repo = repo self.remote_ref_string = remote_ref_string - self._remote = remote + self._remotename_or_url = remotename_or_url self.old_commit_binsha = old_commit_binsha self.summary = summary @@ -185,16 +267,20 @@ class PushInfo(GitdbPushInfo): 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) + return TagReference(self.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)) + remote_ref = Reference(self.repo, self.remote_ref_string) + if '/' in self._remotename_or_url: + sys.stderr.write("Cannot provide RemoteReference instance if it was created from a url instead of of a remote name: %s. Returning Reference instance instead" % sefl._remotename_or_url) + return remote_ref + #END assert correct input + return RemoteReference(self.repo, "refs/remotes/%s/%s" % (str(self._remotename_or_url), remote_ref.name)) else: raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string) # END @classmethod - def _from_line(cls, remote, line): + def _from_line(cls, repo, remotename_or_url, line): """Create a new PushInfo instance as parsed from line which is expected to be like refs/heads/master:refs/heads/master 05d2687..1d0568e""" control_character, from_to, summary = line.split('\t', 3) @@ -212,7 +298,7 @@ class PushInfo(GitdbPushInfo): if flags & cls.DELETED: from_ref = None else: - from_ref = Reference.from_path(remote.repo, from_ref_string) + from_ref = Reference.from_path(repo, from_ref_string) # commit handling, could be message or commit info old_commit_binsha = None @@ -237,38 +323,27 @@ class PushInfo(GitdbPushInfo): if control_character == " ": split_token = ".." old_sha, new_sha = summary.split(' ')[0].split(split_token) - # have to use constructor here as the sha usually is abbreviated - old_commit_binsha = remote.repo.commit(old_sha) + old_commit_binsha = repo.resolve(old_sha) # END message handling - return PushInfo(flags, from_ref, to_ref_string, remote, old_commit_binsha, summary) + return cls(flags, from_ref, to_ref_string, repo, remotename_or_url, old_commit_binsha, summary) -class FetchInfo(GitdbFetchInfo): +class CmdFetchInfo(FetchInfo): """ - 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 - # 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 FETCH_HEAD - info.note # additional notes given by git-fetch intended for the user - info.old_commit_binsha # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, - # field is set to the previous location of ref, otherwise None + Pure python implementation of a FetchInfo interface """ __slots__ = ('ref','old_commit_binsha', 'flags', 'note') # %c %-*s %-*s -> %s (%s) re_fetch_result = re.compile("^\s*(.) (\[?[\w\s\.]+\]?)\s+(.+) -> ([/\w_\+\.-]+)( \(.*\)?$)?") - _flag_map = { '!' : GitdbFetchInfo.ERROR, - '+' : GitdbFetchInfo.FORCED_UPDATE, - '-' : GitdbFetchInfo.TAG_UPDATE, + _flag_map = { '!' : FetchInfo.ERROR, + '+' : FetchInfo.FORCED_UPDATE, + '-' : FetchInfo.TAG_UPDATE, '*' : 0, - '=' : GitdbFetchInfo.HEAD_UPTODATE, - ' ' : GitdbFetchInfo.FAST_FORWARD } + '=' : FetchInfo.HEAD_UPTODATE, + ' ' : FetchInfo.FAST_FORWARD } def __init__(self, ref, flags, note = '', old_commit_binsha = None): """ @@ -295,7 +370,7 @@ class FetchInfo(GitdbFetchInfo): @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. + and return a new CmdFetchInfo object representing this information. We can handle a line as follows "%c %-*s %-*s -> %s%s" @@ -366,7 +441,7 @@ class FetchInfo(GitdbFetchInfo): split_token = '...' if control_character == ' ': split_token = split_token[:-1] - old_commit_binsha = repo.rev_parse(operation.split(split_token)[0]) + old_commit_binsha = repo.resolve(operation.split(split_token)[0]) # END handle refspec # END reference flag handling @@ -443,7 +518,7 @@ class CmdTransportMixin(TransportDB): :param progress: RemoteProgress derived instance or None :param **kwargs: Additional arguments to be passed to the git-push process""" proc = self._git.push(url, refspecs, porcelain=True, as_process=True, **kwargs) - return get_push_info(self, proc, progress or RemoteProgress()) + return get_push_info(self, url, proc, CmdRemoteProgress(progress)) def pull(self, url, refspecs=None, progress=None, **kwargs): """Fetch and merge the given refspecs. @@ -453,7 +528,7 @@ class CmdTransportMixin(TransportDB): :param refspecs: see push() :param progress: see push()""" proc = self._git.pull(url, refspecs, with_extended_output=True, as_process=True, v=True, **kwargs) - return get_fetch_info_from_stderr(self, proc, progress or RemoteProgress()) + return get_fetch_info_from_stderr(self, proc, CmdRemoteProgress(progress)) def fetch(self, url, refspecs=None, progress=None, **kwargs): """Fetch the latest changes @@ -461,7 +536,7 @@ class CmdTransportMixin(TransportDB): :param refspecs: see push() :param progress: see push()""" proc = self._git.fetch(url, refspecs, with_extended_output=True, as_process=True, v=True, **kwargs) - return get_fetch_info_from_stderr(self, proc, progress or RemoteProgress()) + return get_fetch_info_from_stderr(self, proc, CmdRemoteProgress(progress)) #} end transport db interface @@ -699,14 +774,14 @@ class CmdHighLevelRepository(HighLevelRepository): All remaining keyword arguments are given to the git-clone command For more information, see the respective method in HighLevelRepository""" - return self._clone(self.git, self.git_dir, path, progress or RemoteProgress(), **kwargs) + return self._clone(self.git, self.git_dir, path, CmdRemoteProgress(progress), **kwargs) @classmethod def clone_from(cls, url, to_path, progress = None, **kwargs): """ :param kwargs: see the ``clone`` method For more information, see the respective method in the HighLevelRepository""" - return cls._clone(cls.GitCls(os.getcwd()), url, to_path, progress or RemoteProgress(), **kwargs) + return cls._clone(cls.GitCls(os.getcwd()), url, to_path, CmdRemoteProgress(progress), **kwargs) def archive(self, ostream, treeish=None, prefix=None, **kwargs): """For all args see HighLevelRepository interface diff --git a/git/db/interface.py b/git/db/interface.py index a7502e85..30b0c7c1 100644 --- a/git/db/interface.py +++ b/git/db/interface.py @@ -238,6 +238,64 @@ class RefSpec(object): return self.source is None +class RemoteProgress(object): + """ + Handler providing an interface to parse progress information emitted by git-push + and git-fetch and to dispatch callbacks allowing subclasses to react to the progress. + + Subclasses should derive from this type. + """ + _num_op_codes = 5 + BEGIN, END, COUNTING, COMPRESSING, WRITING = [1 << x for x in range(_num_op_codes)] + STAGE_MASK = BEGIN|END + OP_MASK = ~STAGE_MASK + + #{ Subclass Interface + + 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, message='', input=''): + """Called whenever the progress changes + + :param 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. + :param cur_count: Current absolute count of items + + :param 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. + + :param message: + In case of the 'WRITING' operation, it contains the amount of bytes + transferred. It may possibly be used for other purposes as well. + + :param input: + The actual input string that was used to parse the information from. + This is usually a line from the output of git-fetch, but really + depends on the implementation + + You may read the contents of the current line in self._cur_line""" + pass + + def __call__(self, message, input=''): + """Same as update, but with a simpler interface which only provides the + message of the operation. + :note: This method will be called in addition to the update method. It is + up to you which one you implement""" + pass + #} END subclass interface + + class PushInfo(object): """A type presenting information about the result of a push operation for exactly one refspec @@ -248,7 +306,7 @@ class PushInfo(object): remote_ref_string # path to the remote reference located on the remote side remote_ref # Remote Reference on the local side corresponding to # the remote_ref_string. It can be a TagReference as well. - old_commit # commit at which the remote_ref was standing before we pushed + old_commit_binsha # binary sha to commit at which the remote_ref was standing before we pushed # it to local_ref.commit. Will be None if an error was indicated summary # summary line providing human readable english text about the push """ @@ -269,10 +327,8 @@ class FetchInfo(object): # i.e. info.flags & info.REJECTED # is 0 if ref is FETCH_HEAD note # additional notes given by the fetch-pack implementation intended for the user - old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, - # field is set to the previous location of ref as hexsha or None - # Implementors may use their own type too, but it should decay into a - # string of its hexadecimal sha representation""" + old_commit_binsha# if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, + # field is set to the previous location of ref as binary sha or None""" __slots__ = tuple() NEW_TAG, NEW_HEAD, HEAD_UPTODATE, TAG_UPDATE, REJECTED, FORCED_UPDATE, \ diff --git a/git/db/py/base.py b/git/db/py/base.py index a2c9a4ef..2fdbd202 100644 --- a/git/db/py/base.py +++ b/git/db/py/base.py @@ -18,7 +18,7 @@ from git.util import ( expandvars, expanduser, exists, - is_git_dir + is_git_dir, ) from git.index import IndexFile @@ -40,7 +40,7 @@ import os __all__ = ( 'PureObjectDBR', 'PureObjectDBW', 'PureRootPathDB', 'PureCompoundDB', 'PureConfigurationMixin', 'PureRepositoryPathsMixin', 'PureAlternatesFileMixin', 'PureIndexDB') - + class PureObjectDBR(ObjectDBR): @@ -471,3 +471,4 @@ class PureAlternatesFileMixin(object): alternates = property(_get_alternates, _set_alternates, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates") #} END interface + |