From 6cea653f5bab9c396f4ea883a93d965ac01be154 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 20 Dec 2015 11:25:26 +0100 Subject: VCS: Implement GNU Bazaar --- ranger/ext/vcs/bzr.py | 178 +++++++++++++++++++++++++++++++------------------- ranger/ext/vcs/vcs.py | 42 ++++++++++-- 2 files changed, 145 insertions(+), 75 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index a771dbb4..49ce8b34 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -10,106 +10,146 @@ from .vcs import Vcs, VcsError class Bzr(Vcs): """VCS implementation for GNU Bazaar""" - HEAD="last:1" + HEAD = 'last:1' + + _status_translations = ( + ('+ -R', 'K NM', 'staged'), + (' -', 'D', 'deleted'), + ('?', ' ', 'untracked'), + ) # Generic #--------------------------- - def _bzr(self, path, args, silent=True, catchout=False, retbytes=False): - return self._vcs(path, 'bzr', args, silent=silent, catchout=catchout, retbytes=retbytes) - - def _has_head(self): - """Checks whether repo has head""" - rnum = self._bzr(self.path, ['revno'], catchout=True) - return rnum != '0' - - def _sanitize_rev(self, rev): - if rev == None: return None - rev = rev.strip() - if len(rev) == 0: return None + def _bzr(self, args, path=None, catchout=True, retbytes=False): + """Run a bzr command""" + return self._vcs(path or self.path, 'bzr', args, catchout=catchout, retbytes=retbytes) - return rev + def _remote_url(self): + """Returns remote url""" + try: + return self._bzr(['config', 'parent_location']).rstrip() or None + except VcsError: + return None def _log(self, refspec=None, filelist=None): - """Gets a list of dicts containing revision info, for the revisions matching refspec""" - args = ['log', '-n0', '--show-ids'] - if refspec: args = args + ["-r", refspec] + """Returns an array of dicts containing revision info for refspec""" + args = ['log', '--log-format', 'long', '--levels', '0', '--show-ids'] + if refspec: + args += ['--revision', refspec] + if filelist: + args += ['--'] + filelist - if filelist: args = args + filelist - - raw = self._bzr(self.path, args, catchout=True, silent=True) - L = re.findall('-+$(.*?)^-', raw + '\n---', re.MULTILINE | re.DOTALL) + try: + output = self._bzr(args) + except VcsError: + return None + entries = re.findall(r'-+\n(.+?)\n(?:-|\Z)', output, re.MULTILINE | re.DOTALL) log = [] - for t in L: - t = t.strip() - if len(t) == 0: continue - - dt = {} - m = re.search('^revno:\s*([0-9]+)\s*$', t, re.MULTILINE) - if m: dt['short'] = m.group(1).strip() - m = re.search('^revision-id:\s*(.+)\s*$', t, re.MULTILINE) - if m: dt['revid'] = m.group(1).strip() - m = re.search('^committer:\s*(.+)\s*$', t, re.MULTILINE) - if m: dt['author'] = m.group(1).strip() - m = re.search('^timestamp:\s*(.+)\s*$', t, re.MULTILINE) - if m: dt['date'] = datetime.strptime(m.group(1).strip(), '%a %Y-%m-%d %H:%M:%S %z') - m = re.search('^message:\s*^(.+)$', t, re.MULTILINE) - if m: dt['summary'] = m.group(1).strip() - log.append(dt) + for entry in entries: + new = {} + try: + new['short'] = re.search(r'^revno: ([0-9]+)', entry, re.MULTILINE).group(1) + new['revid'] = re.search(r'^revision-id: (.+)$', entry, re.MULTILINE).group(1) + new['author'] = re.search(r'^committer: (.+)$', entry, re.MULTILINE).group(1) + new['date'] = datetime.strptime( + re.search(r'^timestamp: (.+)$', entry, re.MULTILINE).group(1), + '%a %Y-%m-%d %H:%M:%S %z' + ) + new['summary'] = re.search(r'^message:\n (.+)$', entry, re.MULTILINE).group(1) + except AttributeError: + return None + log.append(new) return log - def _bzr_file_status(self, st): - st = st.strip() - if st in "AM": return 'staged' - elif st in "D": return 'deleted' - elif st in "?": return 'untracked' - else: return 'unknown' + def _bzr_status_translate(self, code): + """Translate status code""" + for X, Y, status in self._status_translations: + if code[0] in X and code[1] in Y: + return status + return 'unknown' # Action Interface #--------------------------- def action_add(self, filelist=None): - if filelist != None: self._bzr(self.path, ['add'] + filelist) - else: self._bzr(self.path, ['add']) + args = ['add'] + if filelist: + args += ['--'] + filelist + self._bzr(args, catchout=False) def action_reset(self, filelist=None): - if filelist != None: self._bzr(self.path, ['remove', '--keep', '--new'] + filelist) - else: self._bzr(self.path, ['remove', '--keep', '--new']) + args = ['remove', '--keep', '--new'] + if filelist: + args += ['--'] + filelist + self._bzr(args, catchout=False) # Data Interface #--------------------------- + def data_status_root(self): + statuses = set() + + # Paths with status + for line in self._bzr(['status', '--short', '--no-classify']).splitlines(): + statuses.add(self._bzr_status_translate(line[:2])) + + for status in self.DIRSTATUSES: + if status in statuses: + return status + return 'sync' + def data_status_subpaths(self): - raw = self._bzr(self.path, ['status', '--short', '--no-classify'], catchout=True, retbytes=True) - L = re.findall('^(..)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) - ret = {} - for st, p in L: - sta = self._bzr_file_status(st) - ret[os.path.normpath(p.strip())] = sta - return ret + statuses = {} + + # Ignored + for path in self._bzr(['ls', '--null', '--ignored']).split('\x00')[:-1]: + statuses[path] = 'ignored' + + # Paths with status + for line in self._bzr(['status', '--short', '--no-classify']).splitlines(): + statuses[os.path.normpath(line[4:])] = self._bzr_status_translate(line[:2]) + + return statuses def data_status_remote(self): + if not self._remote_url(): + return 'none' + + # XXX: Find a local solution + ahead = behind = False try: - remote = self._bzr(self.path, ['config', 'parent_location'], catchout=True) + self._bzr(['missing', '--mine-only'], catchout=False) except VcsError: - remote = "" + ahead = True + try: + self._bzr(['missing', '--theirs-only'], catchout=False) + except VcsError: + behind = True - return remote.strip() or None + if ahead: + return 'diverged' if behind else 'ahead' + else: + return 'behind' if behind else 'sync' def data_branch(self): - branch = self._bzr(self.path, ['nick'], catchout=True) - return branch or None + try: + return self._bzr(['nick']).rstrip() or None + except VcsError: + return None def data_info(self, rev=None): - if rev == None: rev = self.HEAD - rev = self._sanitize_rev(rev) - if rev == self.HEAD and not self._has_head(): return [] - - L = self._log(refspec=rev) - if len(L) == 0: - raise VcsError("Revision %s does not exist" % rev) - elif len(L) > 1: - raise VcsError("More than one instance of revision %s ?!?" % rev) + if rev is None: + rev = self.HEAD + + log = self._log(refspec=rev) + if not log: + if rev == self.HEAD: + return None + else: + raise VcsError('Revision {0:s} does not exist'.format(rev)) + elif len(log) == 1: + return log[0] else: - return L[0] + raise VcsError('More than one instance of revision {0:s} ?!?'.format(rev)) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index e2d86d4b..ae770699 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -6,6 +6,7 @@ import os import subprocess import threading +import time class VcsError(Exception): """VCS exception""" @@ -35,10 +36,10 @@ class Vcs(object): # Backends REPOTYPES = { - 'git': {'class': 'Git', 'setting': 'vcs_backend_git'}, - 'hg': {'class': 'Hg', 'setting': 'vcs_backend_hg'}, - 'bzr': {'class': 'Bzr', 'setting': 'vcs_backend_bzr'}, - 'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn'}, + 'git': {'class': 'Git', 'setting': 'vcs_backend_git', 'lazy': False}, + 'hg': {'class': 'Hg', 'setting': 'vcs_backend_hg', 'lazy': True}, + 'bzr': {'class': 'Bzr', 'setting': 'vcs_backend_bzr', 'lazy': True}, + 'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn', 'lazy': True}, } # Possible directory statuses in order of importance with statuses that @@ -73,7 +74,7 @@ class Vcs(object): self.rootvcs = self self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), self.REPOTYPES[self.repotype]['class']) - self.status_subpaths = {} + self.status_subpaths = None if not os.access(self.repodir, os.R_OK): directoryobject.vcsstatus = 'unknown' @@ -88,6 +89,7 @@ class Vcs(object): except VcsError: return + self.timestamp = time.time() self.track = True else: self.rootvcs = directoryobject.fm.get_directory(self.root).vcs @@ -218,6 +220,7 @@ class Vcs(object): except VcsError: self.update_tree(purge=True) return False + self.timestamp = time.time() return True def check(self): @@ -230,8 +233,28 @@ class Vcs(object): return False return True + def check_outdated(self): + """Check if outdated""" + for wroot, wdirs, _ in os.walk(self.path): + wrootobj = self.obj.fm.get_directory(wroot) + wrootobj.load_if_outdated() + if wroot != self.path and wrootobj.vcs.is_root: + wdirs[:] = [] + continue + + if wrootobj.stat and self.timestamp < wrootobj.stat.st_mtime: + return True + if wrootobj.files_all: + for wfile in wrootobj.files_all: + if wfile.stat and self.timestamp < wfile.stat.st_mtime: + return True + return False + def status_root(self): """Returns root status""" + if self.rootvcs.status_subpaths is None: + return 'unknown' + statuses = set(status for path, status in self.status_subpaths.items()) for status in self.DIRSTATUSES: if status in statuses: @@ -244,6 +267,9 @@ class Vcs(object): path needs to be self.obj.path or subpath thereof """ + if self.rootvcs.status_subpaths is None: + return 'unknown' + if path == self.obj.path: relpath = os.path.relpath(self.path, self.root) else: @@ -345,7 +371,11 @@ class VcsThread(threading.Thread): and (clmn.target.path == target.vcs.repodir or clmn.target.path.startswith(target.vcs.repodir + '/'))): continue - if target.vcs.update_root(): + lazy = target.vcs.REPOTYPES[target.vcs.repotype]['lazy'] + if (target.vcs.rootvcs.status_subpaths is None \ + or (lazy and target.vcs.check_outdated()) \ + or not lazy) \ + and target.vcs.update_root(): target.vcs.update_tree() redraw = True -- cgit 1.4.1-2-gfad0