Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

last_change: Use git log --name-status to avoid repeated fork+exec

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
  • Loading branch information...
commit 9541c2acb4b2c9df2d6ca37e299161501c370244 1 parent 0283ffb
Anders Kaseorg andersk authored
Showing with 73 additions and 21 deletions.
  1. +51 −2 tracext/git/PyGIT.py
  2. +22 −19 tracext/git/git_fs.py
53 tracext/git/PyGIT.py
View
@@ -14,6 +14,7 @@
from threading import Lock
from subprocess import Popen, PIPE
from operator import itemgetter
+from contextlib import contextmanager
import cStringIO
import codecs
@@ -70,8 +71,11 @@ def __execute(self, git_cmd, *cmd_args):
def cat_file_batch(self):
return self.__pipe('cat-file', '--batch', stdin=PIPE, stdout=PIPE)
+ def log_pipe(self, *cmd_args):
+ return self.__pipe('log', *cmd_args, stdout=PIPE)
+
def __getattr__(self, name):
- if name[0] == '_' or name in ['cat_file_batch']:
+ if name[0] == '_' or name in ['cat_file_batch', 'log_pipe']:
raise AttributeError, name
return partial(self.__execute, name.replace('_','-'))
@@ -713,7 +717,52 @@ def sync(self):
rev = self.repo.rev_list("--max-count=1", "--topo-order", "--all").strip()
return self.__rev_cache_sync(rev)
- def last_change(self, sha, path):
+ @contextmanager
+ def get_historian(self, sha, base_path):
+ p = []
+ change = {}
+ next_path = []
+
+ def name_status_gen():
+ p[:] = [self.repo.log_pipe('--pretty=format:%n%H', '--name-status',
+ sha, '--', base_path)]
+ f = p[0].stdout
+ for l in f:
+ if l == '\n': continue
+ old_sha = l.rstrip('\n')
+ for l in f:
+ if l == '\n': break
+ _, path = l.rstrip('\n').split('\t', 1)
+ while path not in change:
+ change[path] = old_sha
+ if next_path == [path]: yield old_sha
+ try:
+ path, _ = path.rsplit('/', 1)
+ except ValueError:
+ break
+ f.close()
+ p[0].terminate()
+ p[0].wait()
+ p[:] = []
+ while True: yield None
+ gen = name_status_gen()
+
+ def historian(path):
+ try:
+ return change[path]
+ except KeyError:
+ next_path[:] = [path]
+ return gen.next()
+ yield historian
+
+ if p:
+ p[0].stdout.close()
+ p[0].terminate()
+ p[0].wait()
+
+ def last_change(self, sha, path, historian=None):
+ if historian is not None:
+ return historian(path)
return self.repo.rev_list("--max-count=1",
sha, "--",
self._fs_from_unicode(path)).strip() or None
41 tracext/git/git_fs.py
View
@@ -387,8 +387,8 @@ def display_rev(self, rev):
def short_rev(self, rev):
return self.git.shortrev(self.normalize_rev(rev), min_len=self._shortrev_len)
- def get_node(self, path, rev=None):
- return GitNode(self, path, rev, self.log)
+ def get_node(self, path, rev=None, historian=None):
+ return GitNode(self, path, rev, self.log, None, historian)
def get_quickjump_entries(self, rev):
for bname, bsha in self.git.get_branches():
@@ -412,24 +412,26 @@ def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=0):
if old_path != new_path:
raise TracError("not supported in git_fs")
- for chg in self.git.diff_tree(old_rev, new_rev, self.normalize_path(new_path)):
- mode1, mode2, obj1, obj2, action, path, path2 = chg
+ with self.git.get_historian(old_rev, old_path.strip('/')) as old_historian:
+ with self.git.get_historian(new_rev, new_path.strip('/')) as new_historian:
+ for chg in self.git.diff_tree(old_rev, new_rev, self.normalize_path(new_path)):
+ mode1, mode2, obj1, obj2, action, path, path2 = chg
- kind = Node.FILE
- if mode2.startswith('04') or mode1.startswith('04'):
- kind = Node.DIRECTORY
+ kind = Node.FILE
+ if mode2.startswith('04') or mode1.startswith('04'):
+ kind = Node.DIRECTORY
- change = GitChangeset.action_map[action]
+ change = GitChangeset.action_map[action]
- old_node = None
- new_node = None
+ old_node = None
+ new_node = None
- if change != Changeset.ADD:
- old_node = self.get_node(path, old_rev)
- if change != Changeset.DELETE:
- new_node = self.get_node(path, new_rev)
+ if change != Changeset.ADD:
+ old_node = self.get_node(path, old_rev, old_historian)
+ if change != Changeset.DELETE:
+ new_node = self.get_node(path, new_rev, new_historian)
- yield old_node, new_node, kind, change
+ yield old_node, new_node, kind, change
def next_rev(self, rev, path=''):
return self.git.hist_next_revision(rev)
@@ -469,7 +471,7 @@ def sync(self, rev_callback=None, clean=None):
rev_callback(rev)
class GitNode(Node):
- def __init__(self, repos, path, rev, log, ls_tree_info=None):
+ def __init__(self, repos, path, rev, log, ls_tree_info=None, historian=None):
self.log = log
self.repos = repos
self.fs_sha = None # points to either tree or blobs
@@ -491,7 +493,7 @@ def __init__(self, repos, path, rev, log, ls_tree_info=None):
self.fs_perm, k, self.fs_sha, self.fs_size, _ = ls_tree_info
# fix-up to the last commit-rev that touched this node
- rev = repos.git.last_change(rev, p)
+ rev = repos.git.last_change(rev, p, historian)
if k == 'tree':
pass
@@ -537,8 +539,9 @@ def get_entries(self):
if not self.isdir:
return
- for ent in self.repos.git.ls_tree(self.rev, self.__git_path()):
- yield GitNode(self.repos, ent[-1], self.rev, self.log, ent)
+ with self.repos.git.get_historian(self.rev, self.path.strip('/')) as historian:
+ for ent in self.repos.git.ls_tree(self.rev, self.__git_path()):
+ yield GitNode(self.repos, ent[-1], self.rev, self.log, ent, historian)
def get_content_type(self):
if self.isdir:
Please sign in to comment.
Something went wrong with that request. Please try again.