| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,11 @@ | |||
| 2 | 2 | Changelog | |
| 3 | 3 | ========= | |
| 4 | 4 | ||
| 5 | + 0.3.5 - Bugfixes | ||
| 6 | + ================ | ||
| 7 | + * push/pull/fetch operations will not block anymore | ||
| 8 | + * A list of all fixed issues can be found here: https://github.com/gitpython-developers/GitPython/issues?q=milestone%3A%22v0.3.5+-+bugfixes%22+ | ||
| 9 | + | ||
| 5 | 10 | 0.3.4 - Python 3 Support | |
| 6 | 11 | ======================== | |
| 7 | 12 | * Internally, hexadecimal SHA1 are treated as ascii encoded strings. Binary SHA1 are treated as bytes. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,7 +6,9 @@ | |||
| 6 | 6 | ||
| 7 | 7 | import os | |
| 8 | 8 | import sys | |
| 9 | + import select | ||
| 9 | 10 | import logging | |
| 11 | + import threading | ||
| 10 | 12 | from subprocess import ( | |
| 11 | 13 | call, | |
| 12 | 14 | Popen, | |
@@ -16,7 +18,8 @@ | |||
| 16 | 18 | ||
| 17 | 19 | from .util import ( | |
| 18 | 20 | LazyMixin, | |
| 19 | - stream_copy | ||
| 21 | + stream_copy, | ||
| 22 | + WaitGroup | ||
| 20 | 23 | ) | |
| 21 | 24 | from .exc import GitCommandError | |
| 22 | 25 | from git.compat import ( | |
@@ -36,9 +39,121 @@ | |||
| 36 | 39 | __all__ = ('Git', ) | |
| 37 | 40 | ||
| 38 | 41 | ||
| 42 | + # ============================================================================== | ||
| 43 | + ## @name Utilities | ||
| 44 | + # ------------------------------------------------------------------------------ | ||
| 45 | + # Documentation | ||
| 46 | + ## @{ | ||
| 47 | + | ||
| 48 | + def handle_process_output(process, stdout_handler, stderr_handler, finalizer): | ||
| 49 | + """Registers for notifications to lean that process output is ready to read, and dispatches lines to | ||
| 50 | + the respective line handlers. We are able to handle carriage returns in case progress is sent by that | ||
| 51 | + mean. For performance reasons, we only apply this to stderr. | ||
| 52 | + This function returns once the finalizer returns | ||
| 53 | + :return: result of finalizer | ||
| 54 | + :param process: subprocess.Popen instance | ||
| 55 | + :param stdout_handler: f(stdout_line_string), or None | ||
| 56 | + :param stderr_hanlder: f(stderr_line_string), or None | ||
| 57 | + :param finalizer: f(proc) - wait for proc to finish""" | ||
| 58 | + def read_line_fast(stream): | ||
| 59 | + return stream.readline() | ||
| 60 | + | ||
| 61 | + def read_line_slow(stream): | ||
| 62 | + line = b'' | ||
| 63 | + while True: | ||
| 64 | + char = stream.read(1) # reads individual single byte strings | ||
| 65 | + if not char: | ||
| 66 | + break | ||
| 67 | + | ||
| 68 | + if char in (b'\r', b'\n') and line: | ||
| 69 | + break | ||
| 70 | + else: | ||
| 71 | + line += char | ||
| 72 | + # END process parsed line | ||
| 73 | + # END while file is not done reading | ||
| 74 | + return line | ||
| 75 | + # end | ||
| 76 | + | ||
| 77 | + def dispatch_line(fno): | ||
| 78 | + stream, handler, readline = fdmap[fno] | ||
| 79 | + # this can possibly block for a while, but since we wake-up with at least one or more lines to handle, | ||
| 80 | + # we are good ... | ||
| 81 | + line = readline(stream).decode(defenc) | ||
| 82 | + if line and handler: | ||
| 83 | + handler(line) | ||
| 84 | + return line | ||
| 85 | + # end dispatch helper | ||
| 86 | + # end | ||
| 87 | + | ||
| 88 | + def deplete_buffer(fno, wg=None): | ||
| 89 | + while True: | ||
| 90 | + line = dispatch_line(fno) | ||
| 91 | + if not line: | ||
| 92 | + break | ||
| 93 | + # end deplete buffer | ||
| 94 | + if wg: | ||
| 95 | + wg.done() | ||
| 96 | + # end | ||
| 97 | + | ||
| 98 | + fdmap = {process.stdout.fileno(): (process.stdout, stdout_handler, read_line_fast), | ||
| 99 | + process.stderr.fileno(): (process.stderr, stderr_handler, read_line_slow)} | ||
| 100 | + | ||
| 101 | + if hasattr(select, 'poll'): | ||
| 102 | + # poll is preferred, as select is limited to file handles up to 1024 ... . This could otherwise be | ||
| 103 | + # an issue for us, as it matters how many handles or own process has | ||
| 104 | + poll = select.poll() | ||
| 105 | + READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR | ||
| 106 | + CLOSED = select.POLLHUP | select.POLLERR | ||
| 107 | + | ||
| 108 | + poll.register(process.stdout, READ_ONLY) | ||
| 109 | + poll.register(process.stderr, READ_ONLY) | ||
| 110 | + | ||
| 111 | + closed_streams = set() | ||
| 112 | + while True: | ||
| 113 | + # no timeout | ||
| 114 | + poll_result = poll.poll() | ||
| 115 | + for fd, result in poll_result: | ||
| 116 | + if result & CLOSED: | ||
| 117 | + closed_streams.add(fd) | ||
| 118 | + else: | ||
| 119 | + dispatch_line(fd) | ||
| 120 | + # end handle closed stream | ||
| 121 | + # end for each poll-result tuple | ||
| 122 | + | ||
| 123 | + if len(closed_streams) == len(fdmap): | ||
| 124 | + break | ||
| 125 | + # end its all done | ||
| 126 | + # end endless loop | ||
| 127 | + | ||
| 128 | + # Depelete all remaining buffers | ||
| 129 | + for fno in fdmap.keys(): | ||
| 130 | + deplete_buffer(fno) | ||
| 131 | + # end for each file handle | ||
| 132 | + else: | ||
| 133 | + # Oh ... probably we are on windows. select.select() can only handle sockets, we have files | ||
| 134 | + # The only reliable way to do this now is to use threads and wait for both to finish | ||
| 135 | + # Since the finalizer is expected to wait, we don't have to introduce our own wait primitive | ||
| 136 | + # NO: It's not enough unfortunately, and we will have to sync the threads | ||
| 137 | + wg = WaitGroup() | ||
| 138 | + for fno in fdmap.keys(): | ||
| 139 | + wg.add(1) | ||
| 140 | + t = threading.Thread(target=lambda: deplete_buffer(fno, wg)) | ||
| 141 | + t.start() | ||
| 142 | + # end | ||
| 143 | + # NOTE: Just joining threads can possibly fail as there is a gap between .start() and when it's | ||
| 144 | + # actually started, which could make the wait() call to just return because the thread is not yet | ||
| 145 | + # active | ||
| 146 | + wg.wait() | ||
| 147 | + # end | ||
| 148 | + | ||
| 149 | + return finalizer(process) | ||
| 150 | + | ||
| 151 | + | ||
| 39 | 152 | def dashify(string): | |
| 40 | 153 | return string.replace('_', '-') | |
| 41 | 154 | ||
| 155 | + ## -- End Utilities -- @} | ||
| 156 | + | ||
| 42 | 157 | ||
| 43 | 158 | class Git(LazyMixin): | |
| 44 | 159 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -287,11 +287,11 @@ def from_tree(cls, repo, *treeish, **kwargs): | |||
| 287 | 287 | changes according to the amount of trees. | |
| 288 | 288 | If 1 Tree is given, it will just be read into a new index | |
| 289 | 289 | If 2 Trees are given, they will be merged into a new index using a | |
| 290 | - two way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other' | ||
| 291 | - one. It behaves like a fast-forward. | ||
| 292 | - If 3 Trees are given, a 3-way merge will be performed with the first tree | ||
| 293 | - being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree, | ||
| 294 | - tree 3 is the 'other' one | ||
| 290 | + two way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other' | ||
| 291 | + one. It behaves like a fast-forward. | ||
| 292 | + If 3 Trees are given, a 3-way merge will be performed with the first tree | ||
| 293 | + being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree, | ||
| 294 | + tree 3 is the 'other' one | ||
| 295 | 295 | ||
| 296 | 296 | :param kwargs: | |
| 297 | 297 | Additional arguments passed to git-read-tree | |
@@ -882,14 +882,11 @@ def move(self, items, skip_errors=False, **kwargs): | |||
| 882 | 882 | ||
| 883 | 883 | def commit(self, message, parent_commits=None, head=True, author=None, committer=None): | |
| 884 | 884 | """Commit the current default index file, creating a commit object. | |
| 885 | - | ||
| 886 | 885 | For more information on the arguments, see tree.commit. | |
| 887 | - :note: | ||
| 888 | - If you have manually altered the .entries member of this instance, | ||
| 889 | - don't forget to write() your changes to disk beforehand. | ||
| 890 | 886 | ||
| 891 | - :return: | ||
| 892 | - Commit object representing the new commit""" | ||
| 887 | + :note: If you have manually altered the .entries member of this instance, | ||
| 888 | + don't forget to write() your changes to disk beforehand. | ||
| 889 | + :return: Commit object representing the new commit""" | ||
| 893 | 890 | tree = self.write_tree() | |
| 894 | 891 | return Commit.create_from_tree(self.repo, tree, message, parent_commits, | |
| 895 | 892 | head, author=author, committer=committer) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -31,6 +31,7 @@ | |||
| 31 | 31 | join_path, | |
| 32 | 32 | finalize_process | |
| 33 | 33 | ) | |
| 34 | + from git.cmd import handle_process_output | ||
| 34 | 35 | from gitdb.util import join | |
| 35 | 36 | from git.compat import defenc | |
| 36 | 37 | ||
@@ -40,30 +41,6 @@ | |||
| 40 | 41 | #{ Utilities | |
| 41 | 42 | ||
| 42 | 43 | ||
| 43 | - def digest_process_messages(fh, progress): | ||
| 44 | - """Read progress messages from file-like object fh, supplying the respective | ||
| 45 | - progress messages to the progress instance. | ||
| 46 | - | ||
| 47 | - :param fh: File handle to read from | ||
| 48 | - :return: list(line, ...) list of lines without linebreaks that did | ||
| 49 | - not contain progress information""" | ||
| 50 | - line_so_far = b'' | ||
| 51 | - dropped_lines = list() | ||
| 52 | - while True: | ||
| 53 | - char = fh.read(1) # reads individual single byte strings | ||
| 54 | - if not char: | ||
| 55 | - break | ||
| 56 | - | ||
| 57 | - if char in (b'\r', b'\n') and line_so_far: | ||
| 58 | - dropped_lines.extend(progress._parse_progress_line(line_so_far.decode(defenc))) | ||
| 59 | - line_so_far = b'' | ||
| 60 | - else: | ||
| 61 | - line_so_far += char | ||
| 62 | - # END process parsed line | ||
| 63 | - # END while file is not done reading | ||
| 64 | - return dropped_lines | ||
| 65 | - | ||
| 66 | - | ||
| 67 | 44 | def add_progress(kwargs, git, progress): | |
| 68 | 45 | """Add the --progress flag to the given kwargs dict if supported by the | |
| 69 | 46 | git command. If the actual progress in the given progress instance is not | |
@@ -532,17 +509,25 @@ def _get_fetch_info_from_stderr(self, proc, progress): | |||
| 532 | 509 | # Basically we want all fetch info lines which appear to be in regular form, and thus have a | |
| 533 | 510 | # command character. Everything else we ignore, | |
| 534 | 511 | cmds = set(PushInfo._flag_map.keys()) & set(FetchInfo._flag_map.keys()) | |
| 535 | - for line in digest_process_messages(proc.stderr, progress): | ||
| 536 | - if line.startswith('fatal:'): | ||
| 537 | - raise GitCommandError(("Error when fetching: %s" % line,), 2) | ||
| 538 | - # END handle special messages | ||
| 539 | - for cmd in cmds: | ||
| 540 | - if line[1] == cmd: | ||
| 541 | - fetch_info_lines.append(line) | ||
| 542 | - continue | ||
| 543 | - # end find command code | ||
| 544 | - # end for each comand code we know | ||
| 545 | - # END for each line | ||
| 512 | + | ||
| 513 | + progress_handler = progress.new_message_handler() | ||
| 514 | + | ||
| 515 | + def my_progress_handler(line): | ||
| 516 | + for pline in progress_handler(line): | ||
| 517 | + if line.startswith('fatal:'): | ||
| 518 | + raise GitCommandError(("Error when fetching: %s" % line,), 2) | ||
| 519 | + # END handle special messages | ||
| 520 | + for cmd in cmds: | ||
| 521 | + if line[1] == cmd: | ||
| 522 | + fetch_info_lines.append(line) | ||
| 523 | + continue | ||
| 524 | + # end find command code | ||
| 525 | + # end for each comand code we know | ||
| 526 | + # end for each line progress didn't handle | ||
| 527 | + # end | ||
| 528 | + | ||
| 529 | + # We are only interested in stderr here ... | ||
| 530 | + handle_process_output(proc, None, my_progress_handler, finalize_process) | ||
| 546 | 531 | ||
| 547 | 532 | # read head information | |
| 548 | 533 | fp = open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') | |
@@ -555,7 +540,6 @@ def _get_fetch_info_from_stderr(self, proc, progress): | |||
| 555 | 540 | ||
| 556 | 541 | output.extend(FetchInfo._from_line(self.repo, err_line, fetch_line) | |
| 557 | 542 | for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info)) | |
| 558 | - finalize_process(proc) | ||
| 559 | 543 | return output | |
| 560 | 544 | ||
| 561 | 545 | def _get_push_info(self, proc, progress): | |
@@ -564,19 +548,19 @@ def _get_push_info(self, proc, progress): | |||
| 564 | 548 | # read the lines manually as it will use carriage returns between the messages | |
| 565 | 549 | # to override the previous one. This is why we read the bytes manually | |
| 566 | 550 | # TODO: poll() on file descriptors to know what to read next, process streams concurrently | |
| 567 | - digest_process_messages(proc.stderr, progress) | ||
| 568 | - | ||
| 551 | + progress_handler = progress.new_message_handler() | ||
| 569 | 552 | output = IterableList('name') | |
| 570 | - for line in proc.stdout.readlines(): | ||
| 571 | - line = line.decode(defenc) | ||
| 553 | + | ||
| 554 | + def stdout_handler(line): | ||
| 572 | 555 | try: | |
| 573 | 556 | output.append(PushInfo._from_line(self, line)) | |
| 574 | 557 | except ValueError: | |
| 575 | 558 | # if an error happens, additional info is given which we cannot parse | |
| 576 | 559 | pass | |
| 577 | 560 | # END exception handling | |
| 578 | 561 | # END for each line | |
| 579 | - finalize_process(proc) | ||
| 562 | + | ||
| 563 | + handle_process_output(proc, stdout_handler, progress_handler, finalize_process) | ||
| 580 | 564 | return output | |
| 581 | 565 | ||
| 582 | 566 | def fetch(self, refspec=None, progress=None, **kwargs): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,7 +5,10 @@ | |||
| 5 | 5 | # the BSD License: http://www.opensource.org/licenses/bsd-license.php | |
| 6 | 6 | ||
| 7 | 7 | from git.exc import InvalidGitRepositoryError, NoSuchPathError | |
| 8 | - from git.cmd import Git | ||
| 8 | + from git.cmd import ( | ||
| 9 | + Git, | ||
| 10 | + handle_process_output | ||
| 11 | + ) | ||
| 9 | 12 | from git.refs import ( | |
| 10 | 13 | HEAD, | |
| 11 | 14 | Head, | |
@@ -25,7 +28,6 @@ | |||
| 25 | 28 | from git.config import GitConfigParser | |
| 26 | 29 | from git.remote import ( | |
| 27 | 30 | Remote, | |
| 28 | - digest_process_messages, | ||
| 29 | 31 | add_progress | |
| 30 | 32 | ) | |
| 31 | 33 | ||
@@ -711,9 +713,10 @@ def _clone(cls, git, url, path, odb_default_type, progress, **kwargs): | |||
| 711 | 713 | proc = git.clone(url, path, with_extended_output=True, as_process=True, | |
| 712 | 714 | v=True, **add_progress(kwargs, git, progress)) | |
| 713 | 715 | if progress: | |
| 714 | - digest_process_messages(proc.stderr, progress) | ||
| 715 | - # END handle progress | ||
| 716 | - finalize_process(proc) | ||
| 716 | + handle_process_output(proc, None, progress.new_message_handler(), finalize_process) | ||
| 717 | + else: | ||
| 718 | + finalize_process(proc) | ||
| 719 | + # end handle progress | ||
| 717 | 720 | finally: | |
| 718 | 721 | if prev_cwd is not None: | |
| 719 | 722 | os.chdir(prev_cwd) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,6 +12,7 @@ | |||
| 12 | 12 | import shutil | |
| 13 | 13 | import platform | |
| 14 | 14 | import getpass | |
| 15 | + import threading | ||
| 15 | 16 | ||
| 16 | 17 | # NOTE: Some of the unused imports might be used/imported by others. | |
| 17 | 18 | # Handle once test-cases are back up and running. | |
@@ -32,7 +33,7 @@ | |||
| 32 | 33 | __all__ = ("stream_copy", "join_path", "to_native_path_windows", "to_native_path_linux", | |
| 33 | 34 | "join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList", | |
| 34 | 35 | "BlockingLockFile", "LockFile", 'Actor', 'get_user_id', 'assure_directory_exists', | |
| 35 | - 'RemoteProgress', 'rmtree') | ||
| 36 | + 'RemoteProgress', 'rmtree', 'WaitGroup') | ||
| 36 | 37 | ||
| 37 | 38 | #{ Utility Methods | |
| 38 | 39 | ||
@@ -249,6 +250,14 @@ def _parse_progress_line(self, line): | |||
| 249 | 250 | # END for each sub line | |
| 250 | 251 | return failed_lines | |
| 251 | 252 | ||
| 253 | + def new_message_handler(self): | ||
| 254 | + """:return: a progress handler suitable for handle_process_output(), passing lines on to this Progress | ||
| 255 | + handler in a suitable format""" | ||
| 256 | + def handler(line): | ||
| 257 | + return self._parse_progress_line(line.rstrip()) | ||
| 258 | + # end | ||
| 259 | + return handler | ||
| 260 | + | ||
| 252 | 261 | def line_dropped(self, line): | |
| 253 | 262 | """Called whenever a line could not be understood and was therefore dropped.""" | |
| 254 | 263 | pass | |
@@ -691,3 +700,32 @@ def iter_items(cls, repo, *args, **kwargs): | |||
| 691 | 700 | raise NotImplementedError("To be implemented by Subclass") | |
| 692 | 701 | ||
| 693 | 702 | #} END classes | |
| 703 | + | ||
| 704 | + | ||
| 705 | + class WaitGroup(object): | ||
| 706 | + """WaitGroup is like Go sync.WaitGroup. | ||
| 707 | + | ||
| 708 | + Without all the useful corner cases. | ||
| 709 | + By Peter Teichman, taken from https://gist.github.com/pteichman/84b92ae7cef0ab98f5a8 | ||
| 710 | + """ | ||
| 711 | + def __init__(self): | ||
| 712 | + self.count = 0 | ||
| 713 | + self.cv = threading.Condition() | ||
| 714 | + | ||
| 715 | + def add(self, n): | ||
| 716 | + self.cv.acquire() | ||
| 717 | + self.count += n | ||
| 718 | + self.cv.release() | ||
| 719 | + | ||
| 720 | + def done(self): | ||
| 721 | + self.cv.acquire() | ||
| 722 | + self.count -= 1 | ||
| 723 | + if self.count == 0: | ||
| 724 | + self.cv.notify_all() | ||
| 725 | + self.cv.release() | ||
| 726 | + | ||
| 727 | + def wait(self): | ||
| 728 | + self.cv.acquire() | ||
| 729 | + while self.count > 0: | ||
| 730 | + self.cv.wait() | ||
| 731 | + self.cv.release() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments