From 142db64add27d8f7fc6247fd93a1a7d72fbb64f8 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 4 Oct 2015 19:29:32 +0200 Subject: VCS: Refactor get_status_allfiles, get_ignore_allfiles Use NUL separated output Update _git_file_status --- ranger/ext/vcs/git.py | 61 +++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index f4950822..bba1bfee 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -76,19 +76,22 @@ class Git(Vcs): return log - def _git_file_status(self, st): - if len(st) != 2: raise VcsError("Wrong git file status string: %s" % st) - X, Y = (st[0], st[1]) - if X in " " and Y in " " : return 'sync' - elif X in "MADRC" and Y in " " : return 'staged' - elif X in "MADRC " and Y in "M": return 'changed' - elif X in "MARC " and Y in "D": return 'deleted' - elif X in "U" or Y in "U": return 'conflict' - elif X in "A" and Y in "A": return 'conflict' - elif X in "D" and Y in "D": return 'conflict' - elif X in "?" and Y in "?": return 'untracked' - elif X in "!" and Y in "!": return 'ignored' - else: return 'unknown' + def _git_file_status(self, status): + """ Translate git status code """ + X, Y = (status[0], status[1]) + + if X in "MADRC" and Y in " " : return 'staged' + elif X in " MADRC" and Y in "M" : return 'changed' + elif X in " MARC" and Y in "D" : return 'deleted' + + elif X in "D" and Y in "DU" : return 'conflict' + elif X in "A" and Y in "AU" : return 'conflict' + elif X in "U" and Y in "ADU" : return 'conflict' + + elif X in "?" and Y in "?" : return 'untracked' + elif X in "!" and Y in "!" : return 'ignored' + + else : return 'unknown' @@ -167,25 +170,25 @@ class Git(Vcs): #--------------------------- def get_status_allfiles(self): - """Returns a dict indexed by files not in sync their status as values. - Paths are given relative to the root. Strips trailing '/' from dirs.""" - raw = self._git(self.path, ['status', '--porcelain'], catchout=True, bytes=True) - L = re.findall('^(..)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) - ret = {} - for st, p in L: - sta = self._git_file_status(st) - if 'R' in st: - m = re.match('^(.*)\->(.*)$', p) - if m: p = m.group(2).strip() - ret[os.path.normpath(p.strip())] = sta - return ret + """ Returs a dict (path: status) for paths not in sync. Strips trailing '/' from dirs """ + output = self._git(self.path, ['status', '--porcelain', '-z'], catchout=True, bytes=True)\ + .decode('utf-8').split('\x00')[:-1] + output.reverse() + statuses = [] + while output: + line = output.pop() + statuses.append((line[:2], line[3:])) + if line.startswith('R'): + output.pop() + + return {os.path.normpath(tup[1]): self._git_file_status(tup[0]) for tup in statuses} def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs.""" - raw = self._git(self.path, ['ls-files', '--others', '--directory', '-i', '--exclude-standard'], - catchout=True) - return set(os.path.normpath(p) for p in raw.split('\n')) + """ Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs. """ + output = self._git(self.path, ['ls-files', '--others', '--directory', '--ignored', '--exclude-standard', '-z'], + catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1] + return set(os.path.normpath(p) for p in output) def get_remote_status(self): -- cgit 1.4.1-2-gfad0 From e7e867685eaebb0d12e9e69b74b18cb565b84f10 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 4 Oct 2015 22:12:21 +0200 Subject: VCS: Fix _path_contains --- ranger/ext/vcs/vcs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 3f7236f5..c37bf006 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -84,7 +84,7 @@ class Vcs(object): def _path_contains(self, parent, path): """Checks wether path is an object belonging to the subtree in parent""" if parent == path: return True - parent = os.path.normpath(parent + '/') + parent = os.path.normpath(parent) + '/' path = os.path.normpath(path) return os.path.commonprefix([parent, path]) == parent @@ -237,7 +237,6 @@ class Vcs(object): prel, tail = os.path.split(prel) # check if prel is a directory that contains some file in status - prel = os.path.relpath(path, self.root) if os.path.isdir(path): sts = set(st for p, st in self.status.items() if self._path_contains(path, os.path.join(self.root, p))) -- cgit 1.4.1-2-gfad0 From 96dd8db40de076e751adcba65931b981438c1de2 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 5 Oct 2015 14:50:23 +0200 Subject: VCS: Git --- ranger/container/directory.py | 28 +-- ranger/container/fsobject.py | 83 +-------- ranger/ext/vcs/__init__.py | 4 +- ranger/ext/vcs/bzr.py | 2 +- ranger/ext/vcs/git.py | 312 ++++++++++++++++---------------- ranger/ext/vcs/hg.py | 2 +- ranger/ext/vcs/svn.py | 2 +- ranger/ext/vcs/vcs.py | 351 +++++++++++++++++------------------- ranger/gui/widgets/browsercolumn.py | 22 +-- ranger/gui/widgets/statusbar.py | 21 ++- 10 files changed, 362 insertions(+), 465 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 52b494d5..486507b9 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -18,6 +18,7 @@ from ranger.ext.accumulator import Accumulator from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable from ranger.container.settings import LocalSettings +from ranger.ext.vcs.vcs import Vcs def sort_by_basename(path): """returns path.relative_path (for sorting)""" @@ -109,6 +110,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): content_outdated = False content_loaded = False + vcs = None has_vcschild = False _cumulative_size_calculated = False @@ -145,6 +147,9 @@ class Directory(FileSystemObject, Accumulator, Loadable): self.settings = LocalSettings(path, self.settings) + if self.settings.vcs_aware: + self.vcs = Vcs(self) + self.use() def request_resort(self): @@ -229,7 +234,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): filters.append(lambda file: temporary_filter_search(file.basename)) self.files = [f for f in self.files_all if accept_file(f, filters)] - + # A fix for corner cases when the user invokes show_hidden on a # directory that contains only hidden directories and hidden files. if self.files and not self.pointed_obj: @@ -301,9 +306,9 @@ class Directory(FileSystemObject, Accumulator, Loadable): files = [] disk_usage = 0 - if self.settings.vcs_aware: - self.has_vcschild = False - self.load_vcs(None) + if self.settings.vcs_aware and self.vcs.root: + self.has_vcschild = True + self.vcs.update(self) for name in filenames: try: @@ -329,23 +334,24 @@ class Directory(FileSystemObject, Accumulator, Loadable): except: item = Directory(name, preload=stats, path_is_abs=True) item.load() + if item.settings.vcs_aware: + if item.vcs.is_root: + self.has_vcschild = True + item.vcs.update(item) + elif item.vcs.root: + item.vcs.update_child(item) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) item.load() disk_usage += item.size - - # Load vcs data - if self.settings.vcs_aware: - item.load_vcs(self) - if item.vcs_enabled: - self.has_vcschild = True + if self.settings.vcs_aware and self.vcs.root: + item.vcsfilestatus = self.vcs.get_path_status(item.path) files.append(item) self.percent = 100 * len(files) // len(filenames) yield self.disk_usage = disk_usage - self.vcs_outdated = False self.filenames = filenames self.files_all = files diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index aa848b7a..e396a75a 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -73,14 +73,7 @@ class FileSystemObject(FileManagerAware, SettingsAware): size = 0 - (vcs, - vcsfilestatus, - vcsremotestatus, - vcsbranch, - vcshead) = (None,) * 5 - - vcs_outdated = False - vcs_enabled = False + vcsfilestatus = None basename_is_rel_to = None @@ -241,78 +234,6 @@ class FileSystemObject(FileManagerAware, SettingsAware): return None # it is impossible to get the link destination return self.path - def load_vcs(self, parent): - """ - Reads data regarding the version control system the object is on. - Does not load content specific data. - """ - from ranger.ext.vcs import Vcs, VcsError - - vcs = Vcs(self.path) - - # Not under vcs - if vcs.root == None: - return - - # Already know about the right vcs - elif self.vcs and abspath(vcs.root) == abspath(self.vcs.root): - self.vcs.update() - - # Need new Vcs object and self.path is the root - elif self.vcs == None and abspath(vcs.root) == abspath(self.path): - self.vcs = vcs - self.vcs_outdated = True - - # Otherwise, find the root, and try to get the Vcs object from there - else: - rootdir = self.fm.get_directory(vcs.root) - rootdir.load_if_outdated() - - # Get the Vcs object from rootdir - rootdir.load_vcs(None) - self.vcs = rootdir.vcs - if rootdir.vcs_outdated: - self.vcs_outdated = True - - if self.vcs: - if self.vcs.vcsname == 'git': - backend_state = self.settings.vcs_backend_git - elif self.vcs.vcsname == 'hg': - backend_state = self.settings.vcs_backend_hg - elif self.vcs.vcsname == 'bzr': - backend_state = self.settings.vcs_backend_bzr - elif self.vcs.vcsname == 'svn': - backend_state = self.settings.vcs_backend_svn - else: - backend_state = 'disabled' - - self.vcs_enabled = backend_state in set(['enabled', 'local']) - if self.vcs_enabled: - try: - if self.vcs_outdated or (parent and parent.vcs_outdated): - self.vcs_outdated = False - # this caches the file status for get_file_status(): - self.vcs.get_status() - self.vcsbranch = self.vcs.get_branch() - self.vcshead = self.vcs.get_info(self.vcs.HEAD) - if self.path == self.vcs.root and \ - backend_state == 'enabled': - self.vcsremotestatus = \ - self.vcs.get_remote_status() - elif parent: - self.vcsbranch = parent.vcsbranch - self.vcshead = parent.vcshead - self.vcsfilestatus = self.vcs.get_file_status(self.path) - except VcsError as err: - self.vcsbranch = None - self.vcshead = None - self.vcsremotestatus = 'unknown' - self.vcsfilestatus = 'unknown' - self.fm.notify("Can not load vcs data on %s: %s" % - (self.path, err), bad=True) - else: - self.vcs_enabled = False - def load(self): """Loads information about the directory itself. @@ -411,8 +332,6 @@ class FileSystemObject(FileManagerAware, SettingsAware): real_ctime = None if not self.stat or self.stat.st_ctime != real_ctime: self.load() - if self.settings.vcs_aware: - self.vcs_outdated = True return True return False diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index 3f8e8138..a1e12a7a 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -5,8 +5,6 @@ # # vcs - a python module to handle various version control systems -import os - -from .vcs import VcsError, Vcs +from ranger.ext.vcs.vcs import VcsError, Vcs # vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 2a52cf02..c8c4ff4e 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -10,7 +10,7 @@ import re import shutil from datetime import datetime -from .vcs import Vcs, VcsError +from ranger.ext.vcs import Vcs, VcsError class Bzr(Vcs): diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index bba1bfee..86a42502 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -9,187 +9,176 @@ import os import re import shutil from datetime import datetime +import json -from .vcs import Vcs, VcsError - +from ranger.ext.vcs import Vcs, VcsError class Git(Vcs): - vcsname = 'git' + """VCS implementation for Git""" + vcsname = 'git' + _status_combinations = ( + ('MADRC', ' ', 'staged'), + (' MADRC', 'M', 'changed'), + (' MARC', 'D', 'deleted'), + + ('D', 'DU', 'conflict'), + ('A', 'AU', 'conflict'), + ('U', 'ADU', 'conflict'), + + ('?', '?', 'untracked'), + ('!', '!', 'ignored'), + ) # Auxiliar stuff #--------------------------- - def _git(self, path, args, silent=True, catchout=False, bytes=False): - return self._vcs(path, 'git', args, silent=silent, catchout=catchout, bytes=bytes) - + def _git(self, args, path=None, silent=True, catchout=False, bytes=False): + """Call git""" + return self._vcs(path if path else self.path, 'git', args, silent=silent, + catchout=catchout, bytes=bytes) def _has_head(self): """Checks whether repo has head""" try: - self._git(self.path, ['rev-parse', 'HEAD'], silent=True) + self._git(['rev-parse', 'HEAD'], silent=True) except VcsError: return False return True - def _head_ref(self): """Gets HEAD's ref""" - ref = self._git(self.path, ['symbolic-ref', self.HEAD], catchout=True, silent=True) - return ref.strip() or None - + return self._git(['symbolic-ref', self.HEAD], catchout=True, silent=True) or None def _remote_ref(self, ref): """Gets remote ref associated to given ref""" - if ref == None: return None - remote = self._git(self.path, ['for-each-ref', '--format=%(upstream)', ref], catchout=True, silent=True) - return remote.strip() or None - + if ref is None: + return None + return self._git(['for-each-ref', '--format=%(upstream)', ref], + catchout=True, silent=True) \ + or None def _sanitize_rev(self, rev): - if rev == None: return None + """Sanitize revision string""" + if rev is None: + return None return rev.strip() - def _log(self, refspec=None, maxres=None, filelist=None): """Gets a list of dicts containing revision info, for the revisions matching refspec""" - fmt = '--pretty=%h %H%nAuthor: %an <%ae>%nDate: %ct%nSubject: %s%n' - - args = ['--no-pager', 'log', fmt] - if refspec: args = args + ['-1', refspec] - elif maxres: args = args + ['-%d' % maxres] - - if filelist: args = args + ['--'] + filelist - - raw = self._git(self.path, args, catchout=True) - L = re.findall('^\s*(\w*)\s*(\w*)\s*^Author:\s*(.*)\s*^Date:\s*(.*)\s*^Subject:\s*(.*)\s*', raw, re.MULTILINE) + args = [ + '--no-pager', 'log', + '--pretty={"short": "%h", "revid": "%H", "author": "%an", "date": %ct, "summary": "%s"}' + ] + if refspec: + args += ['-1', refspec] + elif maxres: + args += ['-{0:d}'.format(maxres)] + if filelist: + args += ['--'] + filelist log = [] - for t in L: - dt = {} - dt['short'] = t[0].strip() - dt['revid'] = t[1].strip() - dt['author'] = t[2].strip() - m = re.match('\d+(\.\d+)?', t[3].strip()) - dt['date'] = datetime.fromtimestamp(float(m.group(0))) - dt['summary'] = t[4].strip() - log.append(dt) + for line in self._git(args, catchout=True).splitlines(): + line = json.loads(line) + line['date'] = datetime.fromtimestamp(line['date']) + log.append(line) return log - - def _git_file_status(self, status): - """ Translate git status code """ - X, Y = (status[0], status[1]) - - if X in "MADRC" and Y in " " : return 'staged' - elif X in " MADRC" and Y in "M" : return 'changed' - elif X in " MARC" and Y in "D" : return 'deleted' - - elif X in "D" and Y in "DU" : return 'conflict' - elif X in "A" and Y in "AU" : return 'conflict' - elif X in "U" and Y in "ADU" : return 'conflict' - - elif X in "?" and Y in "?" : return 'untracked' - elif X in "!" and Y in "!" : return 'ignored' - - else : return 'unknown' - - + def _git_file_status(self, code): + """Translate git status code""" + for X, Y, status in self._status_combinations: + if code[0] in X and code[1] in Y: + return status + return 'unknown' # Repo creation #--------------------------- def init(self): """Initializes a repo in current path""" - self._git(self.path, ['init']) + self._git(['init']) self.update() - def clone(self, src): """Clones a repo from src""" - name = os.path.basename(self.path) - path = os.path.dirname(self.path) try: os.rmdir(self.path) except OSError: - raise VcsError("Can't clone to %s. It is not an empty directory" % self.path) + raise VcsError("Can't clone to {0:s}: Not an empty directory".format(self.path)) - self._git(path, ['clone', src, name]) + self._git(['clone', src, os.path.basename(self.path)], path=os.path.dirname(self.path)) self.update() - - # Action interface #--------------------------- def commit(self, message): """Commits with a given message""" - self._git(self.path, ['commit', '-m', message]) - + self._git(['commit', '--message', message]) def add(self, filelist=None): """Adds files to the index, preparing for commit""" - if filelist != None: self._git(self.path, ['add', '-A'] + filelist) - else: self._git(self.path, ['add', '-A']) - + if filelist: + self._git(['add', '--all'] + filelist) + else: + self._git(['add', '--all']) def reset(self, filelist=None): """Removes files from the index""" - if filelist != None: self._git(self.path, ['reset'] + filelist) - else: self._git(self.path, ['reset']) - + if filelist: + self._git(['reset'] + filelist) + else: + self._git(['reset']) - def pull(self, br=None): + def pull(self, *args): """Pulls from remote""" - if br: self._git(self.path, ['pull', br]) - else: self._git(self.path, ['pull']) - + self._git(['pull'] + list(args)) - def push(self, br=None): + def push(self, *args): """Pushes to remote""" - if br: self._git(self.path, ['push', br]) - else: self._git(self.path, ['push']) - + self._git(['push'] + list(args)) def checkout(self, rev): """Checks out a branch or revision""" - self._git(self.path, ['checkout', self._sanitize_rev(rev)]) - + self._git(['checkout', self._sanitize_rev(rev)]) def extract_file(self, rev, name, dest): """Extracts a file from a given revision and stores it in dest dir""" if rev == self.INDEX: shutil.copyfile(os.path.join(self.path, name), dest) else: - out = self._git(self.path, ['--no-pager', 'show', '%s:%s' % (self._sanitize_rev(rev), name)], - catchout=True, bytes=True) - with open(dest, 'wb') as fd: fd.write(out) - - + with open(dest, 'wb') as fd: + fd.write( + self._git([ + '--no-pager', 'show', '{0:s}:{1:s}'.format(self._sanitize_rev(rev), name) + ], catchout=True, bytes=True) + ) # Data Interface #--------------------------- def get_status_allfiles(self): - """ Returs a dict (path: status) for paths not in sync. Strips trailing '/' from dirs """ - output = self._git(self.path, ['status', '--porcelain', '-z'], catchout=True, bytes=True)\ - .decode('utf-8').split('\x00')[:-1] - output.reverse() - statuses = [] - while output: - line = output.pop() - statuses.append((line[:2], line[3:])) + """Returs a dict (path: status) for paths not in sync. Strips trailing '/' from dirs""" + statuses = {} + skip = False + for line in self._git(['status', '--porcelain', '-z'], catchout=True, bytes=True)\ + .decode('utf-8').split('\x00')[:-1]: + if skip: + skip = False + continue + statuses[os.path.normpath(line[3:])] = self._git_file_status(line[:2]) if line.startswith('R'): - output.pop() - - return {os.path.normpath(tup[1]): self._git_file_status(tup[0]) for tup in statuses} - + skip = True + return statuses def get_ignore_allfiles(self): - """ Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs. """ - output = self._git(self.path, ['ls-files', '--others', '--directory', '--ignored', '--exclude-standard', '-z'], - catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1] - return set(os.path.normpath(p) for p in output) - + """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs.""" + return set( + os.path.normpath(p) + for p in self._git( + ['ls-files', '--others', '--directory', '--ignored', '--exclude-standard', '-z'], + catchout=True, bytes=True + ).decode('utf-8').split('\x00')[:-1] + ) def get_remote_status(self): """Checks the status of the repo regarding sync state with remote branch""" @@ -198,18 +187,17 @@ class Git(Vcs): remote = self._remote_ref(head) except VcsError: head = remote = None - - if head and remote: - raw = self._git(self.path, ['rev-list', '--left-right', '%s...%s' % (remote, head)], catchout=True) - ahead = re.search("^>", raw, flags=re.MULTILINE) - behind = re.search("^<", raw, flags=re.MULTILINE) - - if ahead and behind: return "diverged" - elif ahead and not behind: return "ahead" - elif not ahead and behind: return "behind" - elif not ahead and not behind: return "sync" - else: return "none" - + if not head or not remote: + return 'none' + + output = self._git(['rev-list', '--left-right', '{0:s}...{1:s}'.format(remote, head)], + catchout=True) + ahead = re.search("^>", output, flags=re.MULTILINE) + behind = re.search("^<", output, flags=re.MULTILINE) + if ahead: + return 'diverged' if behind else 'ahead' + else: + return 'behind' if behind else 'sync' def get_branch(self): """Returns the current named branch, if this makes sense for the backend. None otherwise""" @@ -217,37 +205,38 @@ class Git(Vcs): head = self._head_ref() except VcsError: head = None + if head is None: + return 'detached' - if head: - m = re.match('refs/heads/([^/]*)', head) - if m: return m.group(1).strip() + match = re.match('refs/heads/([^/]+)', head) + if match: + return match.group(1) else: - return "detached" - - return None - + return None def get_log(self, filelist=None, maxres=None): """Get the entire log for the current HEAD""" - if not self._has_head(): return [] + if not self._has_head(): + return [] return self._log(refspec=None, maxres=maxres, filelist=filelist) - def get_raw_log(self, filelist=None): """Gets the raw log as a string""" - if not self._has_head(): return [] + if not self._has_head(): + return [] args = ['log'] - if filelist: args = args + ['--'] + filelist - return self._git(self.path, args, catchout=True) - + if filelist: + args += ['--'] + filelist + return self._git(args, catchout=True) def get_raw_diff(self, refspec=None, filelist=None): """Gets the raw diff as a string""" args = ['diff'] - if refspec: args = args + [refspec] - if filelist: args = args + ['--'] + filelist - return self._git(self.path, args, catchout=True) - + if refspec: + args += [refspec] + if filelist: + args += ['--'] + filelist + return self._git(args, catchout=True) def get_remote(self): """Returns the url for the remote repo attached to head""" @@ -257,49 +246,56 @@ class Git(Vcs): remote = self._remote_ref(ref) except VcsError: ref = remote = None - - if remote: - m = re.match('refs/remotes/([^/]*)/', remote) - if m: - url = self._git(self.path, ['config', '--get', 'remote.%s.url' % m.group(1)], catchout=True) - return url.strip() or None + if not remote: + return None + + match = re.match('refs/remotes/([^/]+)/', remote) + if match: + return self._git(['config', '--get', 'remote.{0:s}.url'.format(match.group(1))], + catchout=True).strip() \ + or None return None def get_revision_id(self, rev=None): """Get a canonical key for the revision rev""" - if rev == None: rev = self.HEAD - elif rev == self.INDEX: return None + if rev is None: + rev = self.HEAD + elif rev == self.INDEX: + return None rev = self._sanitize_rev(rev) - return self._sanitize_rev(self._git(self.path, ['rev-parse', rev], catchout=True)) - + return self._sanitize_rev(self._git(['rev-parse', rev], catchout=True)) def get_info(self, rev=None): """Gets info about the given revision rev""" - if rev == None: rev = self.HEAD + if rev is None: + rev = self.HEAD rev = self._sanitize_rev(rev) - if rev == self.HEAD and not self._has_head(): return None - - 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 == self.HEAD and not self._has_head(): + return None + + log = self._log(refspec=rev) + if len(log) == 0: + raise VcsError("Revision {0:s} does not exist".format(rev)) + elif len(log) > 1: + raise VcsError("More than one instance of revision {0:s} ?!?".format(rev)) else: - return L[0] - + return log[0] def get_files(self, rev=None): """Gets a list of files in revision rev""" - if rev == None: rev = self.HEAD + if rev is None: + rev = self.HEAD rev = self._sanitize_rev(rev) + if rev is None: + return [] - if rev: - if rev == self.INDEX: raw = self._git(self.path, ["ls-files"], catchout=True) - else: raw = self._git(self.path, ['ls-tree', '--name-only', '-r', rev], catchout=True) - return raw.split('\n') + if rev == self.INDEX: + return self._git(['ls-files', '-z'], + catchout=True, bytes=True).decode('utf-8').split('\x00') else: - return [] + return self._git(['ls-tree', '--name-only', '-r', '-z', rev], + catchout=True, bytes=True).decode('utf-8').split('\x00') # vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index b8731dbf..35daff4a 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -14,7 +14,7 @@ try: except ImportError: from ConfigParser import RawConfigParser -from .vcs import Vcs, VcsError +from ranger.ext.vcs import Vcs, VcsError class Hg(Vcs): diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 9bf8698c..78dfb8e9 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -14,7 +14,7 @@ import re import shutil import logging from datetime import datetime -from .vcs import Vcs, VcsError +from ranger.ext.vcs import Vcs, VcsError #logging.basicConfig(filename='rangersvn.log',level=logging.DEBUG, # filemode='w') diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index c37bf006..ac996ef4 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -4,16 +4,15 @@ # Author: Abdó Roig-Maranges , 2011-2012 # # vcs - a python module to handle various version control systems +"""Vcs module""" import os import subprocess -from datetime import datetime - class VcsError(Exception): + """Vcs exception""" pass - class Vcs(object): """ This class represents a version controlled path, abstracting the usual operations from the different supported backends. @@ -36,99 +35,144 @@ class Vcs(object): # the current head and nothing. Every backend should redefine them if the # version control has a similar concept, or implement _sanitize_rev method to # clean the rev before using them - INDEX = "INDEX" - HEAD = "HEAD" - NONE = "NONE" - vcsname = None - - # Possible status responses - FILE_STATUS = ['conflict', 'untracked', 'deleted', 'changed', 'staged', 'ignored', 'sync', 'none', 'unknown'] - REMOTE_STATUS = ['none', 'sync', 'behind', 'ahead', 'diverged', 'unknown'] - - - def __init__(self, path, vcstype=None): - # This is a bit hackish, but I need to import here to avoid circular imports - from .git import Git - from .hg import Hg - from .bzr import Bzr - from .svn import SVN - self.repo_types = {'git': Git, 'hg': Hg, 'bzr': Bzr, 'svn': SVN} + INDEX = 'INDEX' + HEAD = 'HEAD' + NONE = 'NONE' + vcsname = None + + # Possible status responses in order of importance + FILE_STATUS = ( + 'conflict', + 'untracked', + 'deleted', + 'changed', + 'staged', + 'ignored', + 'sync', + 'none', + 'unknown', + ) + REMOTE_STATUS = ( + 'diverged', + 'behind', + 'ahead', + 'sync', + 'none', + 'unknown', + ) + + def __init__(self, directoryobject): + from ranger.ext.vcs.git import Git + from ranger.ext.vcs.hg import Hg + from ranger.ext.vcs.bzr import Bzr + from ranger.ext.vcs.svn import SVN + self.repotypes = { + 'git': Git, + 'hg': Hg, + 'bzr': Bzr, + 'svn': SVN, + } + + self.path = directoryobject.path + self.repotypes_settings = [ + repotype for repotype, setting in \ + ( + ('git', directoryobject.settings.vcs_backend_git), + ('hg', directoryobject.settings.vcs_backend_git), + ('bzr', directoryobject.settings.vcs_backend_git), + ('svn', directoryobject.settings.vcs_backend_git), + ) + if setting in ('enabled', 'local') + ] - self.path = os.path.expanduser(path) self.status = {} self.ignored = set() - self.root = None - - self.update(vcstype=vcstype) - + self.head = None + self.remotestatus = None + self.branch = None + + self.root, self.repotype = self.find_root(self.path) + self.is_root = True if self.path == self.root else False + + if self.root: + # Do not track the repo data directory + repodir = os.path.join(self.root, '.{0:s}'.format(self.repotype)) + if self.path == repodir or self.path.startswith(repodir + '/'): + self.root = None + return + if self.is_root: + self.root = self.path + self.__class__ = self.repotypes[self.repotype] + else: + root = directoryobject.fm.get_directory(self.root) + self.repotype = root.vcs.repotype + self.__class__ = root.vcs.__class__ # Auxiliar #--------------------------- def _vcs(self, path, cmd, args, silent=False, catchout=False, bytes=False): """Executes a vcs command""" - with open('/dev/null', 'w') as devnull: - if silent: out=devnull - else: out=None + with open(os.devnull, 'w') as devnull: + out = devnull if silent else None try: if catchout: - raw = subprocess.check_output([cmd] + args, stderr=out, cwd=path) - if bytes: return raw - else: return raw.decode('utf-8', errors="ignore").strip() + output = subprocess.check_output([cmd] + args, stderr=out, cwd=path) + return output if bytes else output.decode('utf-8').strip() else: subprocess.check_call([cmd] + args, stderr=out, stdout=out, cwd=path) except subprocess.CalledProcessError: - raise VcsError("%s error on %s. Command: %s" % (cmd, path, ' '.join([cmd] + args))) + raise VcsError("{0:s} error on {1:s}. Command: {2:s}"\ + .format(cmd, path, ' '.join([cmd] + args))) - - def _path_contains(self, parent, path): - """Checks wether path is an object belonging to the subtree in parent""" - if parent == path: return True - parent = os.path.normpath(parent) + '/' - path = os.path.normpath(path) - return os.path.commonprefix([parent, path]) == parent - - - # Object manipulation + # Generic #--------------------------- - # This may be a little hacky, but very useful. An instance of Vcs class changes its own class - # when calling update(), to match the right repo type. I can have the same object adapt to - # the path repo type, if this ever changes! - def get_repo_type(self, path): + def get_repotype(self, path): """Returns the right repo type for path. None if no repo present in path""" - for rn, rt in self.repo_types.items(): - if path and os.path.exists(os.path.join(path, '.%s' % rn)): return rt + for repotype in self.repotypes_settings: + if os.path.exists(os.path.join(path, '.{0:s}'.format(repotype))): + return repotype return None - - def get_root(self, path): + def find_root(self, path): """Finds the repository root path. Otherwise returns none""" - curpath = os.path.abspath(path) - while curpath != '/': - if self.get_repo_type(curpath): return curpath - else: curpath = os.path.dirname(curpath) - return None - - - def update(self, vcstype=None): - """Updates the repo instance. Re-checks the repo and changes object class if repo type changes - If vcstype is given, uses that repo type, without autodetection""" - if os.path.exists(self.path): - self.root = self.get_root(self.path) - if vcstype: - if vcstype in self.repo_types: - ty = self.repo_types[vcstype] - else: - raise VcsError("Unrecognized repo type %s" % vcstype) - else: - ty = self.get_repo_type(self.root) - if ty: - self.__class__ = ty - return - - self.__class__ = Vcs - + while True: + repotype = self.get_repotype(path) + if repotype: + return (path, repotype) + if path == '/': + break + path = os.path.dirname(path) + return (None, None) + + def update(self, directoryobject): + """Update repository""" + if self.is_root: + self.head = self.get_info(self.HEAD) + self.branch = self.get_branch() + self.remotestatus = self.get_remote_status() + self.status = self.get_status_allfiles() + self.ignored = self.get_ignore_allfiles() + directoryobject.vcsfilestatus = self.get_root_status() + else: + root = directoryobject.fm.get_directory(self.root) + self.head = root.vcs.head = root.vcs.get_info(root.vcs.HEAD) + self.branch = root.vcs.branch = root.vcs.get_branch() + self.status = root.vcs.status = root.vcs.get_status_allfiles() + self.ignored = root.vcs.ignored = root.vcs.get_ignore_allfiles() + directoryobject.vcsfilestatus = root.vcs.get_path_status( + self.path, is_directory=True) + + def update_child(self, directoryobject): + """After update() for subdirectories""" + root = directoryobject.fm.get_directory(self.root) + self.head = root.vcs.head + self.branch = root.vcs.branch + self.status = root.vcs.status + self.ignored = root.vcs.ignored + directoryobject.vcsfilestatus = root.vcs.get_path_status( + self.path, is_directory=True) # Repo creation #--------------------------- @@ -136,33 +180,31 @@ class Vcs(object): def init(self, repotype): """Initializes a repo in current path""" if not repotype in self.repo_types: - raise VcsError("Unrecognized repo type %s" % repotype) + raise VcsError("Unrecognized repo type {0:s}".format(repotype)) - if not os.path.exists(self.path): os.makedirs(self.path) - rt = self.repo_types[repotype] + if not os.path.exists(self.path): + os.makedirs(self.path) try: - self.__class__ = rt + self.__class__ = self.repo_types[repotype] self.init() except: self.__class__ = Vcs raise - def clone(self, repotype, src): """Clones a repo from src""" if not repotype in self.repo_types: - raise VcsError("Unrecognized repo type %s" % repotype) + raise VcsError("Unrecognized repo type {0:s}".format(repotype)) - if not os.path.exists(self.path): os.makedirs(self.path) - rt = self.repo_types[repotype] + if not os.path.exists(self.path): + os.makedirs(self.path) try: - self.__class__ = rt + self.__class__ = self.repo_types[repotype] self.clone(src) except: self.__class__ = Vcs raise - # Action interface #--------------------------- @@ -170,178 +212,117 @@ class Vcs(object): """Commits with a given message""" raise NotImplementedError - def add(self, filelist): """Adds files to the index, preparing for commit""" raise NotImplementedError - def reset(self, filelist): """Removes files from the index""" raise NotImplementedError - - def pull(self): + def pull(self, **kwargs): """Pulls from remote""" raise NotImplementedError - - def push(self): + def push(self, **kwargs): """Pushes to remote""" raise NotImplementedError - def checkout(self, rev): """Checks out a branch or revision""" raise NotImplementedError - def extract_file(self, rev, name, dest): """Extracts a file from a given revision and stores it in dest dir""" raise NotImplementedError - # Data #--------------------------- def is_repo(self): """Checks wether there is an initialized repo in self.path""" - return self.path and os.path.exists(self.path) and self.root != None - + return self.path and os.path.exists(self.path) and self.root is not None def is_tracking(self): """Checks whether HEAD is tracking a remote repo""" - return self.get_remote(self.HEAD) != None - - - def get_file_status(self, path): - """Returns the status for a given path regarding the repo""" - - # if path is relative, join it with root. otherwise do nothing - path = os.path.join(self.root, path) - - # path is not in the repo - if not self._path_contains(self.root, path): - return "none" - - # check if prel or some parent of prel is ignored - prel = os.path.relpath(path, self.root) - while len(prel) > 0 and prel != '/' and prel != '.': - if prel in self.ignored: return "ignored" - prel, tail = os.path.split(prel) - - # check if prel or some parent of prel is listed in status - prel = os.path.relpath(path, self.root) - while len(prel) > 0 and prel != '/' and prel != '.': - if prel in self.status: return self.status[prel] - prel, tail = os.path.split(prel) - - # check if prel is a directory that contains some file in status - if os.path.isdir(path): - sts = set(st for p, st in self.status.items() - if self._path_contains(path, os.path.join(self.root, p))) - for st in self.FILE_STATUS: - if st in sts: return st - - # it seems prel is in sync - return "sync" - - - def get_status(self, path=None): - """Returns a dict with changed files under path and their status. - If path is None, returns all changed files""" - - self.status = self.get_status_allfiles() - self.ignored = self.get_ignore_allfiles() - if path: - path = os.path.join(self.root, path) - if os.path.commonprefix([self.root, path]) == self.root: - return dict((p, st) for p, st in self.status.items() if self._path_contains(path, os.path.join(self.root, p))) - else: - return {} - else: - return self.status - + return self.get_remote(self.HEAD) is not None + + def get_root_status(self): + """Returns the status of root""" + statuses = set( + status for path, status in self.status.items() + ) + for status in self.FILE_STATUS: + if status in statuses: + return status + return 'sync' + + def get_path_status(self, path, is_directory=False): + """Returns the status of path""" + relpath = os.path.relpath(path, self.root) + + # check if relpath or its parents has a status + tmppath = relpath + while tmppath: + if tmppath in self.ignored: + return 'ignored' + elif tmppath in self.status: + return self.status[tmppath] + tmppath = os.path.dirname(tmppath) + + # check if path contains some file in status + if is_directory: + statuses = set( + status for path, status in self.status.items() + if path.startswith(relpath + '/') + ) + for status in self.FILE_STATUS: + if status in statuses: + return status + + return 'sync' def get_status_allfiles(self): """Returns a dict indexed by files not in sync their status as values. Paths are given relative to the root. Strips trailing '/' from dirs.""" raise NotImplementedError - def get_ignore_allfiles(self): """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs.""" raise NotImplementedError - def get_remote_status(self): """Checks the status of the entire repo""" raise NotImplementedError - def get_branch(self): """Returns the current named branch, if this makes sense for the backend. None otherwise""" raise NotImplementedError - def get_log(self): """Get the entire log for the current HEAD""" raise NotImplementedError - def get_raw_log(self, filelist=None): """Gets the raw log as a string""" raise NotImplementedError - def get_raw_diff(self, refspec=None, filelist=None): """Gets the raw diff as a string""" raise NotImplementedError - def get_remote(self): """Returns the url for the remote repo attached to head""" raise NotImplementedError - def get_revision_id(self, rev=None): """Get a canonical key for the revision rev""" raise NotImplementedError - def get_info(self, rev=None): """Gets info about the given revision rev""" raise NotImplementedError - def get_files(self, rev=None): """Gets a list of files in revision rev""" raise NotImplementedError - - - - # I / O - #--------------------------- - - def print_log(self, fmt): - log = self.log() - if fmt == "compact": - for dt in log: - print(self.format_revision_compact(dt)) - else: - raise Exception("Unknown format %s" % fmt) - - - def format_revision_compact(self, dt): - return "{0:<10}{1:<20}{2}".format(dt['revshort'], - dt['date'].strftime('%a %b %d, %Y'), - dt['summary']) - - - def format_revision_text(self, dt): - L = ["revision: %s:%s" % (dt['revshort'], dt['revhash']), - "date: %s" % dt['date'].strftime('%a %b %d, %Y'), - "time: %s" % dt['date'].strftime('%H:%M'), - "user: %s" % dt['author'], - "description: %s" % dt['summary']] - return '\n'.join(L) diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 52ef62b8..8dff30d1 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -372,25 +372,17 @@ class BrowserColumn(Pager): def _draw_vcsstring_display(self, drawn): vcsstring_display = [] - if self.settings.vcs_aware and (drawn.vcsfilestatus or \ - drawn.vcsremotestatus): + directory = drawn if drawn.is_directory else self.target + if self.settings.vcs_aware and directory.vcs.root: if drawn.vcsfilestatus: vcsstr, vcscol = self.vcsfilestatus_symb[drawn.vcsfilestatus] - else: - vcsstr = " " - vcscol = [] - vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) - - if drawn.vcsremotestatus: - vcsstr, vcscol = self.vcsremotestatus_symb[ - drawn.vcsremotestatus] - else: - - vcsstr = " " - vcscol = [] - vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) + vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) + if drawn.is_directory and drawn.vcs.remotestatus: + vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcs.remotestatus] + vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) elif self.target.has_vcschild: vcsstring_display.append([" ", []]) + return vcsstring_display def _draw_directory_color(self, i, drawn, copied): diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index ee4df028..d6f2c91c 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -181,11 +181,16 @@ class StatusBar(Widget): left.add(strftime(self.timeformat, localtime(stat.st_mtime)), 'mtime') - if target.vcs: - if target.vcsbranch: - vcsinfo = '(%s: %s)' % (target.vcs.vcsname, target.vcsbranch) + if target.settings.vcs_aware: + if target.is_directory and target.vcs.root: + directory = target else: - vcsinfo = '(%s)' % (target.vcs.vcsname) + directory = target.fm.get_directory(os.path.dirname(target.path)) + + if directory.vcs.branch: + vcsinfo = '(%s: %s)' % (directory.vcs.vcsname, directory.vcs.branch) + else: + vcsinfo = '(%s)' % (directory.vcs.vcsname) left.add_space() left.add(vcsinfo, 'vcsinfo') @@ -194,12 +199,12 @@ class StatusBar(Widget): left.add_space() vcsstr, vcscol = self.vcsfilestatus_symb[target.vcsfilestatus] left.add(vcsstr.strip(), 'vcsfile', *vcscol) - if target.vcsremotestatus: - vcsstr, vcscol = self.vcsremotestatus_symb[target.vcsremotestatus] + if directory.vcs.remotestatus: + vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.remotestatus] left.add(vcsstr.strip(), 'vcsremote', *vcscol) - if target.vcshead: + if directory.vcs.head: left.add_space() - left.add('%s' % target.vcshead['summary'], 'vcscommit') + left.add('%s' % directory.vcs.head['summary'], 'vcscommit') def _get_owner(self, target): uid = target.stat.st_uid -- cgit 1.4.1-2-gfad0 From 14a16a1ecef761f22ffcd0ea88128ab5a568353f Mon Sep 17 00:00:00 2001 From: nfnty Date: Wed, 7 Oct 2015 21:34:56 +0200 Subject: VCS: Change vcsfilestatus to vcspathstatus, Fix remotestatus --- ranger/container/directory.py | 2 +- ranger/container/fsobject.py | 2 +- ranger/ext/vcs/vcs.py | 6 +++--- ranger/gui/widgets/__init__.py | 2 +- ranger/gui/widgets/browsercolumn.py | 10 +++++++--- ranger/gui/widgets/statusbar.py | 4 ++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 486507b9..0cf2b528 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -346,7 +346,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): item.load() disk_usage += item.size if self.settings.vcs_aware and self.vcs.root: - item.vcsfilestatus = self.vcs.get_path_status(item.path) + item.vcspathstatus = self.vcs.get_path_status(item.path) files.append(item) self.percent = 100 * len(files) // len(filenames) diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index e396a75a..1a33f1c4 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -73,7 +73,7 @@ class FileSystemObject(FileManagerAware, SettingsAware): size = 0 - vcsfilestatus = None + vcspathstatus = None basename_is_rel_to = None diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index ac996ef4..828ecf14 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -154,14 +154,14 @@ class Vcs(object): self.remotestatus = self.get_remote_status() self.status = self.get_status_allfiles() self.ignored = self.get_ignore_allfiles() - directoryobject.vcsfilestatus = self.get_root_status() + directoryobject.vcspathstatus = self.get_root_status() else: root = directoryobject.fm.get_directory(self.root) self.head = root.vcs.head = root.vcs.get_info(root.vcs.HEAD) self.branch = root.vcs.branch = root.vcs.get_branch() self.status = root.vcs.status = root.vcs.get_status_allfiles() self.ignored = root.vcs.ignored = root.vcs.get_ignore_allfiles() - directoryobject.vcsfilestatus = root.vcs.get_path_status( + directoryobject.vcspathstatus = root.vcs.get_path_status( self.path, is_directory=True) def update_child(self, directoryobject): @@ -171,7 +171,7 @@ class Vcs(object): self.branch = root.vcs.branch self.status = root.vcs.status self.ignored = root.vcs.ignored - directoryobject.vcsfilestatus = root.vcs.get_path_status( + directoryobject.vcspathstatus = root.vcs.get_path_status( self.path, is_directory=True) # Repo creation diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index bd0f2337..a1b4c289 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -5,7 +5,7 @@ from ranger.gui.displayable import Displayable class Widget(Displayable): """A class for classification of widgets.""" - vcsfilestatus_symb = {'conflict': ('X', ["vcsconflict"]), + vcspathstatus_symb = {'conflict': ('X', ["vcsconflict"]), 'untracked': ('+', ["vcschanged"]), 'deleted': ('-', ["vcschanged"]), 'changed': ('*', ["vcschanged"]), diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 8dff30d1..2e31c976 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -257,7 +257,9 @@ class BrowserColumn(Pager): metakey = hash(repr(sorted(metadata.items()))) if metadata else 0 key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, - drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut, + drawn.vcspathstatus, + drawn.vcs.remotestatus if drawn.is_directory and drawn.vcs.is_root else None, + self.fm.do_cut, current_linemode.name, metakey) if key in drawn.display_data: @@ -374,12 +376,14 @@ class BrowserColumn(Pager): vcsstring_display = [] directory = drawn if drawn.is_directory else self.target if self.settings.vcs_aware and directory.vcs.root: - if drawn.vcsfilestatus: - vcsstr, vcscol = self.vcsfilestatus_symb[drawn.vcsfilestatus] + if drawn.vcspathstatus: + vcsstr, vcscol = self.vcspathstatus_symb[drawn.vcspathstatus] vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) if drawn.is_directory and drawn.vcs.remotestatus: vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcs.remotestatus] vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) + elif self.target.vcs.is_root: + vcsstring_display.append([" ", []]) elif self.target.has_vcschild: vcsstring_display.append([" ", []]) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index d6f2c91c..8e5b0376 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -195,9 +195,9 @@ class StatusBar(Widget): left.add_space() left.add(vcsinfo, 'vcsinfo') - if target.vcsfilestatus: + if target.vcspathstatus: left.add_space() - vcsstr, vcscol = self.vcsfilestatus_symb[target.vcsfilestatus] + vcsstr, vcscol = self.vcspathstatus_symb[target.vcspathstatus] left.add(vcsstr.strip(), 'vcsfile', *vcscol) if directory.vcs.remotestatus: vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.remotestatus] -- cgit 1.4.1-2-gfad0 From 4931db4552f2813c248084b46241e488d4655bcc Mon Sep 17 00:00:00 2001 From: nfnty Date: Wed, 7 Oct 2015 23:20:21 +0200 Subject: VCS: Ignore whole directory if all subpaths are ignored --- ranger/ext/vcs/git.py | 2 +- ranger/ext/vcs/vcs.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 86a42502..70fcebe3 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -175,7 +175,7 @@ class Git(Vcs): return set( os.path.normpath(p) for p in self._git( - ['ls-files', '--others', '--directory', '--ignored', '--exclude-standard', '-z'], + ['ls-files', '--others', '--ignored', '--exclude-standard', '-z'], catchout=True, bytes=True ).decode('utf-8').split('\x00')[:-1] ) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 828ecf14..9e7d5e5b 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -273,13 +273,21 @@ class Vcs(object): # check if path contains some file in status if is_directory: statuses = set( - status for path, status in self.status.items() - if path.startswith(relpath + '/') + status for subpath, status in self.status.items() + if subpath.startswith(relpath + '/') ) for status in self.FILE_STATUS: if status in statuses: return status + # check if all subpaths are ignored + for root, _, files in os.walk(path): + for filename in files: + if os.path.relpath(os.path.join(root, filename), self.root) \ + not in self.ignored: + return 'sync' + return 'ignored' + return 'sync' def get_status_allfiles(self): -- cgit 1.4.1-2-gfad0 From cb3654b6de9ead90e63c160b8bdf8ea1d485f95d Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 8 Oct 2015 00:14:37 +0200 Subject: VCS: Fix imports --- ranger/ext/vcs/__init__.py | 11 +---------- ranger/ext/vcs/bzr.py | 2 +- ranger/ext/vcs/git.py | 2 +- ranger/ext/vcs/hg.py | 2 +- ranger/ext/vcs/svn.py | 2 +- ranger/ext/vcs/vcs.py | 17 +++++++++-------- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index a1e12a7a..b639b50f 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -1,10 +1 @@ -# -*- coding: utf-8 -*- -# This file is part of ranger, the console file manager. -# License: GNU GPL version 3, see the file "AUTHORS" for details. -# Author: Abdó Roig-Maranges , 2011 -# -# vcs - a python module to handle various version control systems - -from ranger.ext.vcs.vcs import VcsError, Vcs - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 +"""VCS Extension Package""" diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index c8c4ff4e..2a52cf02 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -10,7 +10,7 @@ import re import shutil from datetime import datetime -from ranger.ext.vcs import Vcs, VcsError +from .vcs import Vcs, VcsError class Bzr(Vcs): diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 70fcebe3..93150588 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -11,7 +11,7 @@ import shutil from datetime import datetime import json -from ranger.ext.vcs import Vcs, VcsError +from .vcs import Vcs, VcsError class Git(Vcs): """VCS implementation for Git""" diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 35daff4a..b8731dbf 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -14,7 +14,7 @@ try: except ImportError: from ConfigParser import RawConfigParser -from ranger.ext.vcs import Vcs, VcsError +from .vcs import Vcs, VcsError class Hg(Vcs): diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 78dfb8e9..9bf8698c 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -14,7 +14,7 @@ import re import shutil import logging from datetime import datetime -from ranger.ext.vcs import Vcs, VcsError +from .vcs import Vcs, VcsError #logging.basicConfig(filename='rangersvn.log',level=logging.DEBUG, # filemode='w') diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 9e7d5e5b..6e092b4d 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -62,15 +62,11 @@ class Vcs(object): ) def __init__(self, directoryobject): - from ranger.ext.vcs.git import Git - from ranger.ext.vcs.hg import Hg - from ranger.ext.vcs.bzr import Bzr - from ranger.ext.vcs.svn import SVN self.repotypes = { - 'git': Git, - 'hg': Hg, - 'bzr': Bzr, - 'svn': SVN, + 'git': ranger.ext.vcs.git.Git, + 'hg': ranger.ext.vcs.hg.Hg, + 'bzr': ranger.ext.vcs.bzr.Bzr, + 'svn': ranger.ext.vcs.svn.SVN, } self.path = directoryobject.path @@ -334,3 +330,8 @@ class Vcs(object): def get_files(self, rev=None): """Gets a list of files in revision rev""" raise NotImplementedError + +import ranger.ext.vcs.git +import ranger.ext.vcs.hg +import ranger.ext.vcs.bzr +import ranger.ext.vcs.svn -- cgit 1.4.1-2-gfad0 From 7485cd37f6f9f7addd482c33a50a224ded46d443 Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 8 Oct 2015 15:11:13 +0200 Subject: VCS: Return all statuses (including ignored) from get_status_allfiles --- ranger/ext/vcs/git.py | 14 ++--------- ranger/ext/vcs/vcs.py | 67 ++++++++++++++++++--------------------------------- 2 files changed, 25 insertions(+), 56 deletions(-) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 93150588..c97cc9e5 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -160,8 +160,8 @@ class Git(Vcs): """Returs a dict (path: status) for paths not in sync. Strips trailing '/' from dirs""" statuses = {} skip = False - for line in self._git(['status', '--porcelain', '-z'], catchout=True, bytes=True)\ - .decode('utf-8').split('\x00')[:-1]: + for line in self._git(['status', '--ignored', '--porcelain', '-z'], + catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1]: if skip: skip = False continue @@ -170,16 +170,6 @@ class Git(Vcs): skip = True return statuses - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs.""" - return set( - os.path.normpath(p) - for p in self._git( - ['ls-files', '--others', '--ignored', '--exclude-standard', '-z'], - catchout=True, bytes=True - ).decode('utf-8').split('\x00')[:-1] - ) - def get_remote_status(self): """Checks the status of the repo regarding sync state with remote branch""" try: diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 6e092b4d..73ac68cc 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -41,13 +41,13 @@ class Vcs(object): vcsname = None # Possible status responses in order of importance - FILE_STATUS = ( + DIR_STATUS = ( 'conflict', 'untracked', 'deleted', 'changed', 'staged', - 'ignored', + # 'ignored', 'sync', 'none', 'unknown', @@ -144,30 +144,28 @@ class Vcs(object): def update(self, directoryobject): """Update repository""" + root = self if self.is_root else directoryobject.fm.get_directory(self.root).vcs + root.head = root.get_info(root.HEAD) + root.branch = root.get_branch() + root.remotestatus = root.get_remote_status() + root.status = root.get_status_allfiles() + if self.is_root: - self.head = self.get_info(self.HEAD) - self.branch = self.get_branch() - self.remotestatus = self.get_remote_status() - self.status = self.get_status_allfiles() - self.ignored = self.get_ignore_allfiles() directoryobject.vcspathstatus = self.get_root_status() else: - root = directoryobject.fm.get_directory(self.root) - self.head = root.vcs.head = root.vcs.get_info(root.vcs.HEAD) - self.branch = root.vcs.branch = root.vcs.get_branch() - self.status = root.vcs.status = root.vcs.get_status_allfiles() - self.ignored = root.vcs.ignored = root.vcs.get_ignore_allfiles() - directoryobject.vcspathstatus = root.vcs.get_path_status( + self.head = root.head + self.branch = root.branch + self.status = root.status + directoryobject.vcspathstatus = root.get_path_status( self.path, is_directory=True) def update_child(self, directoryobject): """After update() for subdirectories""" - root = directoryobject.fm.get_directory(self.root) - self.head = root.vcs.head - self.branch = root.vcs.branch - self.status = root.vcs.status - self.ignored = root.vcs.ignored - directoryobject.vcspathstatus = root.vcs.get_path_status( + root = directoryobject.fm.get_directory(self.root).vcs + self.head = root.head + self.branch = root.branch + self.status = root.status + directoryobject.vcspathstatus = root.get_path_status( self.path, is_directory=True) # Repo creation @@ -245,10 +243,8 @@ class Vcs(object): def get_root_status(self): """Returns the status of root""" - statuses = set( - status for path, status in self.status.items() - ) - for status in self.FILE_STATUS: + statuses = set(status for path, status in self.status.items()) + for status in self.DIR_STATUS: if status in statuses: return status return 'sync' @@ -260,30 +256,17 @@ class Vcs(object): # check if relpath or its parents has a status tmppath = relpath while tmppath: - if tmppath in self.ignored: - return 'ignored' - elif tmppath in self.status: + if tmppath in self.status: return self.status[tmppath] tmppath = os.path.dirname(tmppath) # check if path contains some file in status if is_directory: - statuses = set( - status for subpath, status in self.status.items() - if subpath.startswith(relpath + '/') - ) - for status in self.FILE_STATUS: + statuses = set(status for subpath, status in self.status.items() + if subpath.startswith(relpath + '/')) + for status in self.DIR_STATUS: if status in statuses: return status - - # check if all subpaths are ignored - for root, _, files in os.walk(path): - for filename in files: - if os.path.relpath(os.path.join(root, filename), self.root) \ - not in self.ignored: - return 'sync' - return 'ignored' - return 'sync' def get_status_allfiles(self): @@ -291,10 +274,6 @@ class Vcs(object): Paths are given relative to the root. Strips trailing '/' from dirs.""" raise NotImplementedError - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs.""" - raise NotImplementedError - def get_remote_status(self): """Checks the status of the entire repo""" raise NotImplementedError -- cgit 1.4.1-2-gfad0 From 79cad207f3843576655b2e07e573937fd611e358 Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 8 Oct 2015 16:52:55 +0200 Subject: VCS: Implement directory statuses properly, Change function names --- ranger/ext/vcs/bzr.py | 4 ++-- ranger/ext/vcs/git.py | 35 +++++++++++++++++++++++++++-------- ranger/ext/vcs/hg.py | 6 +++--- ranger/ext/vcs/svn.py | 6 +++--- ranger/ext/vcs/vcs.py | 10 +++++----- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 2a52cf02..b3d631ec 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -148,7 +148,7 @@ class Bzr(Vcs): # Data Interface #--------------------------- - def get_status_allfiles(self): + def get_status_subpaths(self): """Returns a dict indexed by files not in sync their status as values. Paths are given relative to the root. Strips trailing '/' from dirs.""" raw = self._bzr(self.path, ['status', '--short', '--no-classify'], catchout=True, bytes=True) @@ -167,7 +167,7 @@ class Bzr(Vcs): # TODO: slow due to net access - def get_remote_status(self): + def get_status_remote(self): """Checks the status of the repo regarding sync state with remote branch""" if self.get_remote() == None: return "none" diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index c97cc9e5..54250026 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -16,7 +16,7 @@ from .vcs import Vcs, VcsError class Git(Vcs): """VCS implementation for Git""" vcsname = 'git' - _status_combinations = ( + _status_translations = ( ('MADRC', ' ', 'staged'), (' MADRC', 'M', 'changed'), (' MARC', 'D', 'deleted'), @@ -83,9 +83,9 @@ class Git(Vcs): log.append(line) return log - def _git_file_status(self, code): + def _git_status_translate(self, code): """Translate git status code""" - for X, Y, status in self._status_combinations: + for X, Y, status in self._status_translations: if code[0] in X and code[1] in Y: return status return 'unknown' @@ -156,21 +156,40 @@ class Git(Vcs): # Data Interface #--------------------------- - def get_status_allfiles(self): - """Returs a dict (path: status) for paths not in sync. Strips trailing '/' from dirs""" + def get_status_subpaths(self): + """Returns a dict (path: status) for paths not in sync. Strips trailing '/' from dirs""" statuses = {} + + # Ignored directories + for line in self._git( + ['ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'], + catchout=True, bytes=True + ).decode('utf-8').split('\x00')[:-1]: + if line.endswith('/'): + statuses[os.path.normpath(line)] = 'ignored' + + # Empty directories + for line in self._git( + ['ls-files', '-z', '--others', '--directory', '--exclude-standard'], + catchout=True, bytes=True + ).decode('utf-8').split('\x00')[:-1]: + if line.endswith('/'): + statuses[os.path.normpath(line)] = 'none' + + # Paths with status skip = False - for line in self._git(['status', '--ignored', '--porcelain', '-z'], + for line in self._git(['status', '--porcelain', '-z', '--ignored'], catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1]: if skip: skip = False continue - statuses[os.path.normpath(line[3:])] = self._git_file_status(line[:2]) + statuses[os.path.normpath(line[3:])] = self._git_status_translate(line[:2]) if line.startswith('R'): skip = True + return statuses - def get_remote_status(self): + def get_status_remote(self): """Checks the status of the repo regarding sync state with remote branch""" try: head = self._head_ref() diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index b8731dbf..de723878 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -124,7 +124,7 @@ class Hg(Vcs): def reset(self, filelist=None): """Removes files from the index""" - if filelist == None: filelist = self.get_status_allfiles().keys() + if filelist == None: filelist = self.get_status_subpaths().keys() self._hg(self.path, ['forget'] + filelist) @@ -154,7 +154,7 @@ class Hg(Vcs): # Data Interface #--------------------------- - def get_status_allfiles(self): + def get_status_subpaths(self): """Returns a dict indexed by files not in sync their status as values. Paths are given relative to the root. Strips trailing '/' from dirs.""" raw = self._hg(self.path, ['status'], catchout=True, bytes=True) @@ -175,7 +175,7 @@ class Hg(Vcs): return set(L) - def get_remote_status(self): + def get_status_remote(self): """Checks the status of the repo regarding sync state with remote branch""" if self.get_remote() == None: return "none" diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 9bf8698c..579479aa 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -137,7 +137,7 @@ class SVN(Vcs): def reset(self, filelist=None): """Equivalent to svn revert""" - if filelist == None: filelist = self.get_status_allfiles().keys() + if filelist == None: filelist = self.get_status_subpaths().keys() self._svn(self.path, ['revert'] + filelist) @@ -170,7 +170,7 @@ class SVN(Vcs): # Data Interface #--------------------------- - def get_status_allfiles(self): + def get_status_subpaths(self): """Returns a dict indexed by files not in sync their status as values. Paths are given relative to the root. Strips trailing '/' from dirs.""" raw = self._svn(self.path, ['status'], catchout=True, bytes=True) @@ -193,7 +193,7 @@ class SVN(Vcs): return set(L) - def get_remote_status(self): + def get_status_remote(self): """Checks the status of the repo regarding sync state with remote branch. I'm not sure this make sense for SVN so we're just going to return 'sync'""" diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 73ac68cc..7dc171e8 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -49,7 +49,7 @@ class Vcs(object): 'staged', # 'ignored', 'sync', - 'none', + # 'none', 'unknown', ) REMOTE_STATUS = ( @@ -147,8 +147,8 @@ class Vcs(object): root = self if self.is_root else directoryobject.fm.get_directory(self.root).vcs root.head = root.get_info(root.HEAD) root.branch = root.get_branch() - root.remotestatus = root.get_remote_status() - root.status = root.get_status_allfiles() + root.remotestatus = root.get_status_remote() + root.status = root.get_status_subpaths() if self.is_root: directoryobject.vcspathstatus = self.get_root_status() @@ -269,12 +269,12 @@ class Vcs(object): return status return 'sync' - def get_status_allfiles(self): + def get_status_subpaths(self): """Returns a dict indexed by files not in sync their status as values. Paths are given relative to the root. Strips trailing '/' from dirs.""" raise NotImplementedError - def get_remote_status(self): + def get_status_remote(self): """Checks the status of the entire repo""" raise NotImplementedError -- cgit 1.4.1-2-gfad0 From 1ca99394fccbfe1db8678530b1b7ab1c017d2310 Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 9 Oct 2015 00:40:34 +0200 Subject: VCS: Implement cheap subpath repo updating --- ranger/colorschemes/default.py | 2 +- ranger/container/directory.py | 9 +++----- ranger/ext/vcs/git.py | 19 ++++++++++++++++ ranger/ext/vcs/vcs.py | 50 ++++++++++++++++++++---------------------- ranger/gui/context.py | 2 +- ranger/gui/widgets/__init__.py | 2 +- 6 files changed, 49 insertions(+), 35 deletions(-) diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index 80fa9e39..7a248f2e 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -137,7 +137,7 @@ class Default(ColorScheme): elif context.vcsremote and not context.selected: attr &= ~bold - if context.vcssync: + if context.vcssync or context.vcslocal: fg = green elif context.vcsbehind: fg = red diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 0cf2b528..379e0830 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -307,7 +307,6 @@ class Directory(FileSystemObject, Accumulator, Loadable): disk_usage = 0 if self.settings.vcs_aware and self.vcs.root: - self.has_vcschild = True self.vcs.update(self) for name in filenames: @@ -334,19 +333,17 @@ class Directory(FileSystemObject, Accumulator, Loadable): except: item = Directory(name, preload=stats, path_is_abs=True) item.load() - if item.settings.vcs_aware: + if item.settings.vcs_aware and item.vcs.root: + item.vcs.update(item, child=True) if item.vcs.is_root: self.has_vcschild = True - item.vcs.update(item) - elif item.vcs.root: - item.vcs.update_child(item) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) item.load() disk_usage += item.size if self.settings.vcs_aware and self.vcs.root: - item.vcspathstatus = self.vcs.get_path_status(item.path) + item.vcspathstatus = self.vcs.get_status_subpath(item.path) files.append(item) self.percent = 100 * len(files) // len(filenames) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 54250026..d10c5428 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -156,6 +156,25 @@ class Git(Vcs): # Data Interface #--------------------------- + def get_status_root_child(self): + """Returns the status of a child root, very cheap""" + statuses = set() + # Paths with status + skip = False + for line in self._git(['status', '--porcelain', '-z'], + catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1]: + if skip: + skip = False + continue + statuses.add(self._git_status_translate(line[:2])) + if line.startswith('R'): + skip = True + + for status in self.DIR_STATUS: + if status in statuses: + return status + return 'sync' + def get_status_subpaths(self): """Returns a dict (path: status) for paths not in sync. Strips trailing '/' from dirs""" statuses = {} diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 7dc171e8..ebd18385 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -81,8 +81,7 @@ class Vcs(object): if setting in ('enabled', 'local') ] - self.status = {} - self.ignored = set() + self.status_subpaths = {} self.head = None self.remotestatus = None self.branch = None @@ -142,32 +141,27 @@ class Vcs(object): path = os.path.dirname(path) return (None, None) - def update(self, directoryobject): + def update(self, directoryobject, child=False): """Update repository""" root = self if self.is_root else directoryobject.fm.get_directory(self.root).vcs - root.head = root.get_info(root.HEAD) - root.branch = root.get_branch() - root.remotestatus = root.get_status_remote() - root.status = root.get_status_subpaths() + if child and self.is_root: + directoryobject.vcspathstatus = self.get_status_root_child() + elif not child: + root.head = root.get_info(root.HEAD) + root.branch = root.get_branch() + root.status_subpaths = root.get_status_subpaths() + if self.is_root: + directoryobject.vcspathstatus = self.get_status_root() if self.is_root: - directoryobject.vcspathstatus = self.get_root_status() + root.remotestatus = root.get_status_remote() else: self.head = root.head self.branch = root.branch - self.status = root.status - directoryobject.vcspathstatus = root.get_path_status( + self.status_subpaths = root.status_subpaths + directoryobject.vcspathstatus = root.get_status_subpath( self.path, is_directory=True) - def update_child(self, directoryobject): - """After update() for subdirectories""" - root = directoryobject.fm.get_directory(self.root).vcs - self.head = root.head - self.branch = root.branch - self.status = root.status - directoryobject.vcspathstatus = root.get_path_status( - self.path, is_directory=True) - # Repo creation #--------------------------- @@ -241,28 +235,32 @@ class Vcs(object): """Checks whether HEAD is tracking a remote repo""" return self.get_remote(self.HEAD) is not None - def get_root_status(self): + def get_status_root_child(self): + """Returns the status of a child root, very cheap""" + raise NotImplementedError + + def get_status_root(self): """Returns the status of root""" - statuses = set(status for path, status in self.status.items()) + statuses = set(status for path, status in self.status_subpaths.items()) for status in self.DIR_STATUS: if status in statuses: return status return 'sync' - def get_path_status(self, path, is_directory=False): + def get_status_subpath(self, path, is_directory=False): """Returns the status of path""" relpath = os.path.relpath(path, self.root) # check if relpath or its parents has a status tmppath = relpath while tmppath: - if tmppath in self.status: - return self.status[tmppath] + if tmppath in self.status_subpaths: + return self.status_subpaths[tmppath] tmppath = os.path.dirname(tmppath) # check if path contains some file in status if is_directory: - statuses = set(status for subpath, status in self.status.items() + statuses = set(status for subpath, status in self.status_subpaths.items() if subpath.startswith(relpath + '/')) for status in self.DIR_STATUS: if status in statuses: @@ -270,7 +268,7 @@ class Vcs(object): return 'sync' def get_status_subpaths(self): - """Returns a dict indexed by files not in sync their status as values. + """Returns a dict indexed by subpaths not in sync their status as values. Paths are given relative to the root. Strips trailing '/' from dirs.""" raise NotImplementedError diff --git a/ranger/gui/context.py b/ranger/gui/context.py index e5aef06c..66d43d1a 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -19,7 +19,7 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo', 'infostring', 'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit', 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', - 'vcsstaged', 'vcssync', 'vcsbehind', 'vcsahead', 'vcsdiverged'] + 'vcsstaged', 'vcssync', 'vcslocal', 'vcsbehind', 'vcsahead', 'vcsdiverged'] class Context(object): def __init__(self, keys): diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index a1b4c289..4dd28c6d 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -15,7 +15,7 @@ class Widget(Displayable): 'none': (' ', []), 'unknown': ('?', ["vcsunknown"])} - vcsremotestatus_symb = {'none': (' ', []), + vcsremotestatus_symb = {'none': ('⌂', ["vcslocal"]), 'sync': ('=', ["vcssync"]), 'behind': ('<', ["vcsbehind"]), 'ahead': ('>', ["vcsahead"]), -- cgit 1.4.1-2-gfad0 From 9645b056c6fc5d1b8882f500ed5e231ed16c02fa Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 9 Oct 2015 02:29:13 +0200 Subject: VCS: Handle repodir paths --- ranger/container/directory.py | 9 ++++++--- ranger/ext/vcs/vcs.py | 34 +++++++++++++++++++++------------- ranger/gui/widgets/browsercolumn.py | 7 +++++-- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 379e0830..6d6e3e88 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -306,7 +306,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): files = [] disk_usage = 0 - if self.settings.vcs_aware and self.vcs.root: + if self.settings.vcs_aware and \ + self.vcs.root and not self.vcs.in_repodir: self.vcs.update(self) for name in filenames: @@ -333,7 +334,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): except: item = Directory(name, preload=stats, path_is_abs=True) item.load() - if item.settings.vcs_aware and item.vcs.root: + if item.settings.vcs_aware and \ + item.vcs.root and not item.vcs.in_repodir: item.vcs.update(item, child=True) if item.vcs.is_root: self.has_vcschild = True @@ -342,7 +344,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): basename_is_rel_to=basename_is_rel_to) item.load() disk_usage += item.size - if self.settings.vcs_aware and self.vcs.root: + if self.settings.vcs_aware and \ + self.vcs.root and not self.vcs.in_repodir: item.vcspathstatus = self.vcs.get_status_subpath(item.path) files.append(item) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index ebd18385..1764122b 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -86,15 +86,16 @@ class Vcs(object): self.remotestatus = None self.branch = None - self.root, self.repotype = self.find_root(self.path) + self.root, self.repodir, self.repotype = self.find_root(self.path) self.is_root = True if self.path == self.root else False if self.root: - # Do not track the repo data directory - repodir = os.path.join(self.root, '.{0:s}'.format(self.repotype)) - if self.path == repodir or self.path.startswith(repodir + '/'): - self.root = None - return + # Do not track self.repodir or its subpaths + if self.path == self.repodir or self.path.startswith(self.repodir + '/'): + self.in_repodir = True + else: + self.in_repodir = False + if self.is_root: self.root = self.path self.__class__ = self.repotypes[self.repotype] @@ -126,23 +127,30 @@ class Vcs(object): def get_repotype(self, path): """Returns the right repo type for path. None if no repo present in path""" for repotype in self.repotypes_settings: - if os.path.exists(os.path.join(path, '.{0:s}'.format(repotype))): - return repotype - return None + repodir = os.path.join(path, '.{0:s}'.format(repotype)) + if os.path.exists(repodir): + return (repodir, repotype) + return (None, None) def find_root(self, path): """Finds the repository root path. Otherwise returns none""" while True: - repotype = self.get_repotype(path) - if repotype: - return (path, repotype) + repodir, repotype = self.get_repotype(path) + if repodir: + return (path, repodir, repotype) if path == '/': break path = os.path.dirname(path) - return (None, None) + return (None, None, None) def update(self, directoryobject, child=False): """Update repository""" + if not os.path.exists(self.repodir): + self.__init__(directoryobject) + if not self.root: + directoryobject.vcspathstatus = None + return + root = self if self.is_root else directoryobject.fm.get_directory(self.root).vcs if child and self.is_root: directoryobject.vcspathstatus = self.get_status_root_child() diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 2e31c976..50bf2e2f 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -379,11 +379,14 @@ class BrowserColumn(Pager): if drawn.vcspathstatus: vcsstr, vcscol = self.vcspathstatus_symb[drawn.vcspathstatus] vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) + else: + vcsstring_display.append([" ", []]) if drawn.is_directory and drawn.vcs.remotestatus: vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcs.remotestatus] vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) - elif self.target.vcs.is_root: - vcsstring_display.append([" ", []]) + else: + if self.target.has_vcschild: + vcsstring_display.insert(-1, [" ", []]) elif self.target.has_vcschild: vcsstring_display.append([" ", []]) -- cgit 1.4.1-2-gfad0 From 8c1403d6533fb8ef3d651ce026f0a92310f65da5 Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 9 Oct 2015 04:47:32 +0200 Subject: VCS: Implement vcs.track --- ranger/container/directory.py | 9 +++------ ranger/ext/vcs/vcs.py | 28 ++++++++++++++-------------- ranger/gui/widgets/browsercolumn.py | 16 +++++++++------- ranger/gui/widgets/statusbar.py | 14 ++++++-------- 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 6d6e3e88..77c2e250 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -306,8 +306,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): files = [] disk_usage = 0 - if self.settings.vcs_aware and \ - self.vcs.root and not self.vcs.in_repodir: + if self.vcs and self.vcs.track: self.vcs.update(self) for name in filenames: @@ -334,8 +333,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): except: item = Directory(name, preload=stats, path_is_abs=True) item.load() - if item.settings.vcs_aware and \ - item.vcs.root and not item.vcs.in_repodir: + if item.vcs and item.vcs.track: item.vcs.update(item, child=True) if item.vcs.is_root: self.has_vcschild = True @@ -344,8 +342,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): basename_is_rel_to=basename_is_rel_to) item.load() disk_usage += item.size - if self.settings.vcs_aware and \ - self.vcs.root and not self.vcs.in_repodir: + if self.vcs and self.vcs.track: item.vcspathstatus = self.vcs.get_status_subpath(item.path) files.append(item) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 1764122b..ffc581f1 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -74,9 +74,9 @@ class Vcs(object): repotype for repotype, setting in \ ( ('git', directoryobject.settings.vcs_backend_git), - ('hg', directoryobject.settings.vcs_backend_git), - ('bzr', directoryobject.settings.vcs_backend_git), - ('svn', directoryobject.settings.vcs_backend_git), + ('hg', directoryobject.settings.vcs_backend_hg), + ('bzr', directoryobject.settings.vcs_backend_bzr), + ('svn', directoryobject.settings.vcs_backend_svn), ) if setting in ('enabled', 'local') ] @@ -90,19 +90,19 @@ class Vcs(object): self.is_root = True if self.path == self.root else False if self.root: + self.track = True + self.__class__ = self.repotypes[self.repotype] + + if not os.access(self.repodir, os.R_OK): + self.track = False + if self.is_root: + directoryobject.vcspathstatus = 'unknown' + self.remotestatus = 'unknown' # Do not track self.repodir or its subpaths if self.path == self.repodir or self.path.startswith(self.repodir + '/'): - self.in_repodir = True - else: - self.in_repodir = False - - if self.is_root: - self.root = self.path - self.__class__ = self.repotypes[self.repotype] - else: - root = directoryobject.fm.get_directory(self.root) - self.repotype = root.vcs.repotype - self.__class__ = root.vcs.__class__ + self.track = False + else: + self.track = False # Auxiliar #--------------------------- diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 50bf2e2f..dc2c193e 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -258,7 +258,7 @@ class BrowserColumn(Pager): key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, drawn.vcspathstatus, - drawn.vcs.remotestatus if drawn.is_directory and drawn.vcs.is_root else None, + drawn.vcs.remotestatus if drawn.is_directory and drawn.vcs and drawn.vcs.is_root else None, self.fm.do_cut, current_linemode.name, metakey) @@ -375,18 +375,20 @@ class BrowserColumn(Pager): def _draw_vcsstring_display(self, drawn): vcsstring_display = [] directory = drawn if drawn.is_directory else self.target - if self.settings.vcs_aware and directory.vcs.root: - if drawn.vcspathstatus: - vcsstr, vcscol = self.vcspathstatus_symb[drawn.vcspathstatus] - vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) - else: - vcsstring_display.append([" ", []]) + if directory.vcs and \ + (directory.vcs.track or (drawn.is_directory and drawn.vcs.is_root)): if drawn.is_directory and drawn.vcs.remotestatus: vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcs.remotestatus] vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) else: if self.target.has_vcschild: vcsstring_display.insert(-1, [" ", []]) + if drawn.vcspathstatus: + vcsstr, vcscol = self.vcspathstatus_symb[drawn.vcspathstatus] + vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) + else: + vcsstring_display.append([" ", []]) + elif self.target.has_vcschild: vcsstring_display.append([" ", []]) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 8e5b0376..20875ac9 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -181,16 +181,14 @@ class StatusBar(Widget): left.add(strftime(self.timeformat, localtime(stat.st_mtime)), 'mtime') - if target.settings.vcs_aware: - if target.is_directory and target.vcs.root: - directory = target - else: - directory = target.fm.get_directory(os.path.dirname(target.path)) + directory = target if target.is_directory else \ + target.fm.get_directory(os.path.dirname(target.path)) + if directory.vcs and directory.vcs.track: if directory.vcs.branch: - vcsinfo = '(%s: %s)' % (directory.vcs.vcsname, directory.vcs.branch) + vcsinfo = '(%s: %s)' % (directory.vcs.repotype, directory.vcs.branch) else: - vcsinfo = '(%s)' % (directory.vcs.vcsname) + vcsinfo = '(%s)' % (directory.vcs.repotype) left.add_space() left.add(vcsinfo, 'vcsinfo') @@ -199,7 +197,7 @@ class StatusBar(Widget): left.add_space() vcsstr, vcscol = self.vcspathstatus_symb[target.vcspathstatus] left.add(vcsstr.strip(), 'vcsfile', *vcscol) - if directory.vcs.remotestatus: + if target.is_directory and target.vcs.is_root and directory.vcs.remotestatus: vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.remotestatus] left.add(vcsstr.strip(), 'vcsremote', *vcscol) if directory.vcs.head: -- cgit 1.4.1-2-gfad0 From dc1639501e4699186346122fc0038ec59d160931 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sat, 10 Oct 2015 17:51:31 +0200 Subject: VCS: Remove vcsname --- ranger/ext/vcs/bzr.py | 1 - ranger/ext/vcs/git.py | 1 - ranger/ext/vcs/hg.py | 1 - ranger/ext/vcs/svn.py | 1 - ranger/ext/vcs/vcs.py | 1 - 5 files changed, 5 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index b3d631ec..7b457997 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -14,7 +14,6 @@ from .vcs import Vcs, VcsError class Bzr(Vcs): - vcsname = 'bzr' HEAD="last:1" # Auxiliar stuff diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index d10c5428..ee0eb698 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -15,7 +15,6 @@ from .vcs import Vcs, VcsError class Git(Vcs): """VCS implementation for Git""" - vcsname = 'git' _status_translations = ( ('MADRC', ' ', 'staged'), (' MADRC', 'M', 'changed'), diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index de723878..f91d18cc 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -18,7 +18,6 @@ from .vcs import Vcs, VcsError class Hg(Vcs): - vcsname = 'hg' HEAD = 'tip' # Auxiliar stuff #--------------------------- diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 579479aa..64a690c1 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -20,7 +20,6 @@ from .vcs import Vcs, VcsError # filemode='w') class SVN(Vcs): - vcsname = 'svn' HEAD = 'HEAD' # Auxiliar stuff #--------------------------- diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index ffc581f1..67493c34 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -38,7 +38,6 @@ class Vcs(object): INDEX = 'INDEX' HEAD = 'HEAD' NONE = 'NONE' - vcsname = None # Possible status responses in order of importance DIR_STATUS = ( -- cgit 1.4.1-2-gfad0 From 68d747be959614bf1901cff00f2dc29c84ef9886 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sat, 10 Oct 2015 23:48:00 +0200 Subject: VCS: Improve class change --- ranger/container/directory.py | 2 +- ranger/ext/vcs/__init__.py | 2 ++ ranger/ext/vcs/vcs.py | 34 +++++++++++++++------------------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 77c2e250..2c3c9f02 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -18,7 +18,7 @@ from ranger.ext.accumulator import Accumulator from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable from ranger.container.settings import LocalSettings -from ranger.ext.vcs.vcs import Vcs +from ranger.ext.vcs import Vcs def sort_by_basename(path): """returns path.relative_path (for sorting)""" diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index b639b50f..8adc11c6 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -1 +1,3 @@ """VCS Extension Package""" + +from .vcs import Vcs, VcsError diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 67493c34..94aada5d 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -39,7 +39,15 @@ class Vcs(object): HEAD = 'HEAD' NONE = 'NONE' - # Possible status responses in order of importance + 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'}, + } + + # Possible status responses in order of importance with statuses that + # don't make sense disabled DIR_STATUS = ( 'conflict', 'untracked', @@ -61,24 +69,11 @@ class Vcs(object): ) def __init__(self, directoryobject): - self.repotypes = { - 'git': ranger.ext.vcs.git.Git, - 'hg': ranger.ext.vcs.hg.Hg, - 'bzr': ranger.ext.vcs.bzr.Bzr, - 'svn': ranger.ext.vcs.svn.SVN, - } - self.path = directoryobject.path - self.repotypes_settings = [ - repotype for repotype, setting in \ - ( - ('git', directoryobject.settings.vcs_backend_git), - ('hg', directoryobject.settings.vcs_backend_hg), - ('bzr', directoryobject.settings.vcs_backend_bzr), - ('svn', directoryobject.settings.vcs_backend_svn), - ) - if setting in ('enabled', 'local') - ] + self.repotypes_settings = set( + repotype for repotype, values in self.REPOTYPES.items() + if getattr(directoryobject.settings, values['setting']) in ('enabled', 'local') + ) self.status_subpaths = {} self.head = None @@ -90,7 +85,8 @@ class Vcs(object): if self.root: self.track = True - self.__class__ = self.repotypes[self.repotype] + self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), + self.REPOTYPES[self.repotype]['class']) if not os.access(self.repodir, os.R_OK): self.track = False -- cgit 1.4.1-2-gfad0 From 82798dccec1aed24e40efe4a731af70eba9fc079 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sat, 10 Oct 2015 23:59:49 +0200 Subject: VCS: Comment out REMOTE_STATUS --- ranger/ext/vcs/vcs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 94aada5d..0ff3fe03 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -59,14 +59,14 @@ class Vcs(object): # 'none', 'unknown', ) - REMOTE_STATUS = ( - 'diverged', - 'behind', - 'ahead', - 'sync', - 'none', - 'unknown', - ) + # REMOTE_STATUS = ( + # 'diverged', + # 'behind', + # 'ahead', + # 'sync', + # 'none', + # 'unknown', + # ) def __init__(self, directoryobject): self.path = directoryobject.path -- cgit 1.4.1-2-gfad0 From 6d24d8cdcd6ef1b004f7d6d92374b9297819b503 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 11 Oct 2015 16:11:13 +0200 Subject: VCS: All data is stored in root --- ranger/container/directory.py | 9 ++-- ranger/ext/vcs/git.py | 4 +- ranger/ext/vcs/vcs.py | 85 ++++++++++++++++++------------------- ranger/gui/widgets/browsercolumn.py | 11 ++--- ranger/gui/widgets/statusbar.py | 21 +++++---- 5 files changed, 63 insertions(+), 67 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 2c3c9f02..95dbd730 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -306,8 +306,10 @@ class Directory(FileSystemObject, Accumulator, Loadable): files = [] disk_usage = 0 - if self.vcs and self.vcs.track: - self.vcs.update(self) + if self.vcs: + self.vcs.check() + if self.vcs.track: + self.vcs.update() for name in filenames: try: @@ -334,9 +336,10 @@ class Directory(FileSystemObject, Accumulator, Loadable): item = Directory(name, preload=stats, path_is_abs=True) item.load() if item.vcs and item.vcs.track: - item.vcs.update(item, child=True) if item.vcs.is_root: self.has_vcschild = True + else: + item.vcspathstatus = self.vcs.get_status_subpath(item.path) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index ee0eb698..0a091064 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -155,8 +155,8 @@ class Git(Vcs): # Data Interface #--------------------------- - def get_status_root_child(self): - """Returns the status of a child root, very cheap""" + def get_status_root_cheap(self): + """Returns the status of root, very cheap""" statuses = set() # Paths with status skip = False diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 0ff3fe03..1bd101ed 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -69,33 +69,37 @@ class Vcs(object): # ) def __init__(self, directoryobject): + self.obj = directoryobject self.path = directoryobject.path self.repotypes_settings = set( repotype for repotype, values in self.REPOTYPES.items() if getattr(directoryobject.settings, values['setting']) in ('enabled', 'local') ) - - self.status_subpaths = {} - self.head = None - self.remotestatus = None - self.branch = None - self.root, self.repodir, self.repotype = self.find_root(self.path) self.is_root = True if self.path == self.root else False if self.root: - self.track = True - self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), - self.REPOTYPES[self.repotype]['class']) - - if not os.access(self.repodir, os.R_OK): - self.track = False - if self.is_root: + if self.is_root: + self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), + self.REPOTYPES[self.repotype]['class']) + self.status_subpaths = {} + self.track = True + self.initiated = False + self.head = self.get_info(self.HEAD) + self.branch = self.get_branch() + self.remotestatus = self.get_status_remote() + self.obj.vcspathstatus = self.get_status_root_cheap() + + if not os.access(self.repodir, os.R_OK): + self.track = False directoryobject.vcspathstatus = 'unknown' self.remotestatus = 'unknown' - # Do not track self.repodir or its subpaths - if self.path == self.repodir or self.path.startswith(self.repodir + '/'): - self.track = False + else: + # Do not track self.repodir or its subpaths + if self.path == self.repodir or self.path.startswith(self.repodir + '/'): + self.track = False + else: + self.track = directoryobject.fm.get_directory(self.root).vcs.track else: self.track = False @@ -138,31 +142,23 @@ class Vcs(object): path = os.path.dirname(path) return (None, None, None) - def update(self, directoryobject, child=False): - """Update repository""" - if not os.path.exists(self.repodir): - self.__init__(directoryobject) - if not self.root: - directoryobject.vcspathstatus = None - return - - root = self if self.is_root else directoryobject.fm.get_directory(self.root).vcs - if child and self.is_root: - directoryobject.vcspathstatus = self.get_status_root_child() - elif not child: - root.head = root.get_info(root.HEAD) - root.branch = root.get_branch() - root.status_subpaths = root.get_status_subpaths() - if self.is_root: - directoryobject.vcspathstatus = self.get_status_root() + def check(self): + """Check repository health""" + if (self.track and not os.path.exists(self.repodir)) \ + or not self.track: + self.__init__(self.obj) - if self.is_root: - root.remotestatus = root.get_status_remote() - else: - self.head = root.head - self.branch = root.branch - self.status_subpaths = root.status_subpaths - directoryobject.vcspathstatus = root.get_status_subpath( + def update(self): + """Update repository""" + root = self.obj.fm.get_directory(self.root).vcs + root.head = root.get_info(self.HEAD) + root.branch = root.get_branch() + root.status_subpaths = root.get_status_subpaths() + root.remotestatus = root.get_status_remote() + root.obj.vcspathstatus = root.get_status_root() + + if not self.is_root: + self.obj.vcspathstatus = root.get_status_subpath( self.path, is_directory=True) # Repo creation @@ -238,7 +234,7 @@ class Vcs(object): """Checks whether HEAD is tracking a remote repo""" return self.get_remote(self.HEAD) is not None - def get_status_root_child(self): + def get_status_root_cheap(self): """Returns the status of a child root, very cheap""" raise NotImplementedError @@ -252,18 +248,19 @@ class Vcs(object): def get_status_subpath(self, path, is_directory=False): """Returns the status of path""" + root = self.obj.fm.get_directory(self.root).vcs relpath = os.path.relpath(path, self.root) # check if relpath or its parents has a status tmppath = relpath while tmppath: - if tmppath in self.status_subpaths: - return self.status_subpaths[tmppath] + if tmppath in root.status_subpaths: + return root.status_subpaths[tmppath] tmppath = os.path.dirname(tmppath) # check if path contains some file in status if is_directory: - statuses = set(status for subpath, status in self.status_subpaths.items() + statuses = set(status for subpath, status in root.status_subpaths.items() if subpath.startswith(relpath + '/')) for status in self.DIR_STATUS: if status in statuses: diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index dc2c193e..40f804ba 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -375,20 +375,17 @@ class BrowserColumn(Pager): def _draw_vcsstring_display(self, drawn): vcsstring_display = [] directory = drawn if drawn.is_directory else self.target - if directory.vcs and \ - (directory.vcs.track or (drawn.is_directory and drawn.vcs.is_root)): - if drawn.is_directory and drawn.vcs.remotestatus: + if directory.vcs and directory.vcs.track: + if drawn.is_directory and drawn.vcs.is_root: vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcs.remotestatus] vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) - else: - if self.target.has_vcschild: - vcsstring_display.insert(-1, [" ", []]) + elif self.target.has_vcschild: + vcsstring_display.insert(-1, [" ", []]) if drawn.vcspathstatus: vcsstr, vcscol = self.vcspathstatus_symb[drawn.vcspathstatus] vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) else: vcsstring_display.append([" ", []]) - elif self.target.has_vcschild: vcsstring_display.append([" ", []]) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 20875ac9..b5e3b178 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -183,26 +183,25 @@ class StatusBar(Widget): directory = target if target.is_directory else \ target.fm.get_directory(os.path.dirname(target.path)) - if directory.vcs and directory.vcs.track: - if directory.vcs.branch: - vcsinfo = '(%s: %s)' % (directory.vcs.repotype, directory.vcs.branch) + vcsroot = directory.fm.get_directory(directory.vcs.root).vcs + if vcsroot.branch: + vcsinfo = '({0:s}: {1:s})'.format(vcsroot.repotype, vcsroot.branch) else: - vcsinfo = '(%s)' % (directory.vcs.repotype) - + vcsinfo = '({0:s})'.format(vcsroot.repotype) left.add_space() left.add(vcsinfo, 'vcsinfo') + left.add_space() + if vcsroot.remotestatus: + vcsstr, vcscol = self.vcsremotestatus_symb[vcsroot.remotestatus] + left.add(vcsstr.strip(), 'vcsremote', *vcscol) if target.vcspathstatus: - left.add_space() vcsstr, vcscol = self.vcspathstatus_symb[target.vcspathstatus] left.add(vcsstr.strip(), 'vcsfile', *vcscol) - if target.is_directory and target.vcs.is_root and directory.vcs.remotestatus: - vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.remotestatus] - left.add(vcsstr.strip(), 'vcsremote', *vcscol) - if directory.vcs.head: + if vcsroot.head: left.add_space() - left.add('%s' % directory.vcs.head['summary'], 'vcscommit') + left.add('{0:s}'.format(vcsroot.head['summary']), 'vcscommit') def _get_owner(self, target): uid = target.stat.st_uid -- cgit 1.4.1-2-gfad0 From 92ed0b31b775dfa25bf417520c830273142a185e Mon Sep 17 00:00:00 2001 From: nfnty Date: Tue, 13 Oct 2015 20:38:27 +0200 Subject: VCS: Separate VCS Thread --- ranger/container/directory.py | 16 ++-- ranger/ext/vcs/__init__.py | 2 +- ranger/ext/vcs/vcs.py | 149 +++++++++++++++++++++++++++++------- ranger/gui/ui.py | 11 +++ ranger/gui/widgets/browsercolumn.py | 6 +- ranger/gui/widgets/browserview.py | 3 + ranger/gui/widgets/statusbar.py | 18 +++-- 7 files changed, 160 insertions(+), 45 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 95dbd730..9725ae45 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -306,10 +306,9 @@ class Directory(FileSystemObject, Accumulator, Loadable): files = [] disk_usage = 0 - if self.vcs: - self.vcs.check() - if self.vcs.track: - self.vcs.update() + if self.vcs and self.vcs.track and not self.vcs.is_root: + self.vcspathstatus = self.vcs.get_status_subpath( + self.path, is_directory=True) for name in filenames: try: @@ -339,7 +338,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): if item.vcs.is_root: self.has_vcschild = True else: - item.vcspathstatus = self.vcs.get_status_subpath(item.path) + item.vcspathstatus = self.vcs.get_status_subpath( + item.path, is_directory=True) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) @@ -384,6 +384,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): finally: self.loading = False self.fm.signal_emit("finished_loading_dir", directory=self) + if self.vcs: + self.fm.ui.vcsthread.wakeup() def unload(self): self.loading = False @@ -422,7 +424,6 @@ class Directory(FileSystemObject, Accumulator, Loadable): pass self.load_generator = None - def sort(self): """Sort the contained files""" if self.files_all is None: @@ -588,7 +589,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): def load_content_if_outdated(self, *a, **k): """Load the contents of the directory if outdated""" - if self.load_content_once(*a, **k): return True + if self.load_content_once(*a, **k): + return True if self.files_all is None or self.content_outdated: self.load_content(*a, **k) diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index 8adc11c6..c6c691b8 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -1,3 +1,3 @@ """VCS Extension Package""" -from .vcs import Vcs, VcsError +from .vcs import Vcs, VcsError, VcsThread diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 1bd101ed..dd74ff82 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -8,6 +8,7 @@ import os import subprocess +import threading class VcsError(Exception): """Vcs exception""" @@ -82,26 +83,35 @@ class Vcs(object): if self.is_root: self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), self.REPOTYPES[self.repotype]['class']) + self.rootvcs = self self.status_subpaths = {} - self.track = True - self.initiated = False - self.head = self.get_info(self.HEAD) - self.branch = self.get_branch() - self.remotestatus = self.get_status_remote() - self.obj.vcspathstatus = self.get_status_root_cheap() - - if not os.access(self.repodir, os.R_OK): + self.in_repodir = False + try: + self.head = self.get_info(self.HEAD) + self.branch = self.get_branch() + self.remotestatus = self.get_status_remote() + self.obj.vcspathstatus = self.get_status_root_cheap() + except VcsError: + return + + if os.access(self.repodir, os.R_OK): + self.track = True + else: self.track = False directoryobject.vcspathstatus = 'unknown' self.remotestatus = 'unknown' else: + self.rootvcs = directoryobject.fm.get_directory(self.root).vcs # Do not track self.repodir or its subpaths if self.path == self.repodir or self.path.startswith(self.repodir + '/'): self.track = False + self.in_repodir = True else: - self.track = directoryobject.fm.get_directory(self.root).vcs.track + self.track = self.rootvcs.track + self.in_repodir = False else: self.track = False + self.in_repodir = False # Auxiliar #--------------------------- @@ -144,22 +154,65 @@ class Vcs(object): def check(self): """Check repository health""" - if (self.track and not os.path.exists(self.repodir)) \ - or not self.track: + if not self.in_repodir \ + and (not self.track or (not self.is_root and self.get_repotype(self.path)[0])): self.__init__(self.obj) + return True + elif self.track and not os.path.exists(self.repodir): + self.update_tree(purge=True) + return False - def update(self): + def update_root(self): """Update repository""" - root = self.obj.fm.get_directory(self.root).vcs - root.head = root.get_info(self.HEAD) - root.branch = root.get_branch() - root.status_subpaths = root.get_status_subpaths() - root.remotestatus = root.get_status_remote() - root.obj.vcspathstatus = root.get_status_root() - - if not self.is_root: - self.obj.vcspathstatus = root.get_status_subpath( - self.path, is_directory=True) + try: + self.rootvcs.head = self.rootvcs.get_info(self.HEAD) + self.rootvcs.branch = self.rootvcs.get_branch() + self.rootvcs.status_subpaths = self.rootvcs.get_status_subpaths() + self.rootvcs.remotestatus = self.rootvcs.get_status_remote() + self.rootvcs.obj.vcspathstatus = self.rootvcs.get_status_root() + except VcsError: + self.update_tree(purge=True) + + def update_tree(self, purge=False): + """Update repository tree""" + for wroot, wdirs, _ in os.walk(self.rootvcs.path): + # Only update loaded directories + try: + wroot_obj = self.obj.fm.directories[wroot] + except KeyError: + wdirs[:] = [] + continue + if wroot_obj.content_loaded: + for fileobj in wroot_obj.files_all: + if purge: + if fileobj.is_directory: + fileobj.vcspathstatus = None + fileobj.vcs.__init__(fileobj) + else: + fileobj.vcspathstatus = None + continue + + if fileobj.is_directory: + fileobj.vcs.check() + if not fileobj.vcs.track: + continue + if not fileobj.vcs.is_root: + fileobj.vcspathstatus = self.rootvcs.get_status_subpath( + fileobj.path, is_directory=True) + else: + fileobj.vcspathstatus = self.rootvcs.get_status_subpath(fileobj.path) + + # Remove dead directories + for wdir in wdirs.copy(): + try: + wdir_obj = self.obj.fm.directories[os.path.join(wroot, wdir)] + except KeyError: + wdirs.remove(wdir) + continue + if wdir_obj.vcs.is_root or not wdir_obj.vcs.track: + wdirs.remove(wdir) + if purge: + self.rootvcs.__init__(self.rootvcs.obj) # Repo creation #--------------------------- @@ -248,19 +301,18 @@ class Vcs(object): def get_status_subpath(self, path, is_directory=False): """Returns the status of path""" - root = self.obj.fm.get_directory(self.root).vcs relpath = os.path.relpath(path, self.root) # check if relpath or its parents has a status tmppath = relpath while tmppath: - if tmppath in root.status_subpaths: - return root.status_subpaths[tmppath] + if tmppath in self.rootvcs.status_subpaths: + return self.rootvcs.status_subpaths[tmppath] tmppath = os.path.dirname(tmppath) # check if path contains some file in status if is_directory: - statuses = set(status for subpath, status in root.status_subpaths.items() + statuses = set(status for subpath, status in self.rootvcs.status_subpaths.items() if subpath.startswith(relpath + '/')) for status in self.DIR_STATUS: if status in statuses: @@ -308,6 +360,51 @@ class Vcs(object): """Gets a list of files in revision rev""" raise NotImplementedError +class VcsThread(threading.Thread): + """Vcs thread""" + def __init__(self, ui, idle_delay): + super(VcsThread, self).__init__(daemon=True) + self.ui = ui + self.delay = idle_delay / 1000 + self.wake = threading.Event() + + def run(self): + # Set for already updated roots + roots = set() + redraw = False + while True: + for column in self.ui.browser.columns: + target = column.target + if target and target.is_directory and target.vcs: + # Redraw if tree is purged + if not target.vcs.check(): + redraw = True + if target.vcs.track and not target.vcs.root in roots: + roots.add(target.vcs.root) + # Do not update repo when repodir is displayed (causes strobing) + if tuple(clmn for clmn in self.ui.browser.columns + if clmn.target + and (clmn.target.path == target.vcs.repodir or + clmn.target.path.startswith(target.vcs.repodir + '/'))): + continue + target.vcs.update_root() + target.vcs.update_tree() + redraw = True + if redraw: + redraw = False + for column in self.ui.browser.columns: + column.need_redraw = True + self.ui.status.need_redraw = True + self.ui.redraw() + roots.clear() + + self.wake.clear() + self.wake.wait(timeout=self.delay) + + def wakeup(self): + """Wakeup thread""" + self.wake.set() + import ranger.ext.vcs.git import ranger.ext.vcs.hg import ranger.ext.vcs.bzr diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 2eacbc4d..f1e1ebbb 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -5,6 +5,7 @@ import os import sys import curses import _curses +import threading from .displayable import DisplayableContainer from .mouse_event import MouseEvent @@ -40,6 +41,7 @@ class UI(DisplayableContainer): def __init__(self, env=None, fm=None): self.keybuffer = KeyBuffer() self.keymaps = KeyMaps(self.keybuffer) + self.redrawlock = threading.Event() if fm is not None: self.fm = fm @@ -218,6 +220,7 @@ class UI(DisplayableContainer): from ranger.gui.widgets.statusbar import StatusBar from ranger.gui.widgets.taskview import TaskView from ranger.gui.widgets.pager import Pager + from ranger.ext.vcs import VcsThread # Create a title bar self.titlebar = TitleBar(self.win) @@ -248,8 +251,15 @@ class UI(DisplayableContainer): self.pager.visible = False self.add_child(self.pager) + # Create VCS thread + self.vcsthread = VcsThread(self, self.settings.idle_delay) + self.vcsthread.start() + def redraw(self): """Redraw all widgets""" + if self.redrawlock.is_set(): + return + self.redrawlock.set() self.poke() # determine which widgets are shown @@ -264,6 +274,7 @@ class UI(DisplayableContainer): self.draw() self.finalize() + self.redrawlock.clear() def redraw_window(self): """Redraw the window. This only calls self.win.redrawwin().""" diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 40f804ba..8961f48f 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -150,8 +150,8 @@ class BrowserColumn(Pager): self.old_thisfile = self.target.pointed_obj if self.target.load_content_if_outdated() \ - or self.target.sort_if_outdated() \ - or self.last_redraw_time < self.target.last_update_time: + or self.target.sort_if_outdated() \ + or self.last_redraw_time < self.target.last_update_time: self.need_redraw = True if self.need_redraw: @@ -258,7 +258,7 @@ class BrowserColumn(Pager): key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, drawn.vcspathstatus, - drawn.vcs.remotestatus if drawn.is_directory and drawn.vcs and drawn.vcs.is_root else None, + drawn.vcs.remotestatus if drawn.is_directory and drawn.vcs and drawn.vcs.track and drawn.vcs.is_root else None, self.fm.do_cut, current_linemode.name, metakey) diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index e9640208..56b9eba7 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -21,6 +21,7 @@ class BrowserView(Widget, DisplayableContainer): old_collapse = False draw_hints = False draw_info = False + vcsthread = None def __init__(self, win, ratios, preview = True): DisplayableContainer.__init__(self, win) @@ -91,6 +92,8 @@ class BrowserView(Widget, DisplayableContainer): self.need_redraw = True self.need_clear = False for tab in self.fm.tabs.values(): + if tab == self.fm.thistab: + continue directory = tab.thisdir if directory: directory.load_content_if_outdated() diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index b5e3b178..538757fd 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -93,6 +93,9 @@ class StatusBar(Widget): self.old_ctime = ctime self.need_redraw = True + if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: + self.need_redraw = True + if self.need_redraw: self.need_redraw = False @@ -184,24 +187,23 @@ class StatusBar(Widget): directory = target if target.is_directory else \ target.fm.get_directory(os.path.dirname(target.path)) if directory.vcs and directory.vcs.track: - vcsroot = directory.fm.get_directory(directory.vcs.root).vcs - if vcsroot.branch: - vcsinfo = '({0:s}: {1:s})'.format(vcsroot.repotype, vcsroot.branch) + if directory.vcs.rootvcs.branch: + vcsinfo = '({0:s}: {1:s})'.format(directory.vcs.rootvcs.repotype, directory.vcs.rootvcs.branch) else: - vcsinfo = '({0:s})'.format(vcsroot.repotype) + vcsinfo = '({0:s})'.format(directory.vcs.rootvcs.repotype) left.add_space() left.add(vcsinfo, 'vcsinfo') left.add_space() - if vcsroot.remotestatus: - vcsstr, vcscol = self.vcsremotestatus_symb[vcsroot.remotestatus] + if directory.vcs.rootvcs.remotestatus: + vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.rootvcs.remotestatus] left.add(vcsstr.strip(), 'vcsremote', *vcscol) if target.vcspathstatus: vcsstr, vcscol = self.vcspathstatus_symb[target.vcspathstatus] left.add(vcsstr.strip(), 'vcsfile', *vcscol) - if vcsroot.head: + if directory.vcs.rootvcs.head: left.add_space() - left.add('{0:s}'.format(vcsroot.head['summary']), 'vcscommit') + left.add('{0:s}'.format(directory.vcs.rootvcs.head['summary']), 'vcscommit') def _get_owner(self, target): uid = target.stat.st_uid -- cgit 1.4.1-2-gfad0 From bfbb84ff426810590be5e10f625a7f14ac75a3b2 Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 16 Oct 2015 09:07:10 +0200 Subject: VCS: Escape special characters in git summary --- ranger/ext/vcs/git.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 0a091064..214fe63e 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -66,7 +66,7 @@ class Git(Vcs): """Gets a list of dicts containing revision info, for the revisions matching refspec""" args = [ '--no-pager', 'log', - '--pretty={"short": "%h", "revid": "%H", "author": "%an", "date": %ct, "summary": "%s"}' + '--pretty={%x00short%x00: %x00%h%x00, %x00revid%x00: %x00%H%x00, %x00author%x00: %x00%an <%ae>%x00, %x00date%x00: %ct, %x00summary%x00: %x00%s%x00}' ] if refspec: args += ['-1', refspec] @@ -76,7 +76,8 @@ class Git(Vcs): args += ['--'] + filelist log = [] - for line in self._git(args, catchout=True).splitlines(): + for line in self._git(args, catchout=True)\ + .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines(): line = json.loads(line) line['date'] = datetime.fromtimestamp(line['date']) log.append(line) -- cgit 1.4.1-2-gfad0 From 0e6ee82588abd0ff098b2e0cef0bd3ec88569689 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sat, 17 Oct 2015 19:59:22 +0200 Subject: VCS: Fix redraw race condition --- ranger/ext/vcs/vcs.py | 3 ++- ranger/gui/ui.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index dd74ff82..8516bd71 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -393,7 +393,8 @@ class VcsThread(threading.Thread): if redraw: redraw = False for column in self.ui.browser.columns: - column.need_redraw = True + if column.target and column.target.is_directory: + column.need_redraw = True self.ui.status.need_redraw = True self.ui.redraw() roots.clear() diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index f1e1ebbb..49c99777 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -42,6 +42,7 @@ class UI(DisplayableContainer): self.keybuffer = KeyBuffer() self.keymaps = KeyMaps(self.keybuffer) self.redrawlock = threading.Event() + self.redrawlock.set() if fm is not None: self.fm = fm @@ -257,9 +258,8 @@ class UI(DisplayableContainer): def redraw(self): """Redraw all widgets""" - if self.redrawlock.is_set(): - return - self.redrawlock.set() + self.redrawlock.wait() + self.redrawlock.clear() self.poke() # determine which widgets are shown @@ -274,7 +274,7 @@ class UI(DisplayableContainer): self.draw() self.finalize() - self.redrawlock.clear() + self.redrawlock.set() def redraw_window(self): """Redraw the window. This only calls self.win.redrawwin().""" -- cgit 1.4.1-2-gfad0 From c4abb96c41ed3dd9688f32198d2cd2e8a3320b4e Mon Sep 17 00:00:00 2001 From: nfnty Date: Sat, 17 Oct 2015 21:53:30 +0200 Subject: VCS: Handle FileNotFoundError during subprocess calls --- ranger/ext/vcs/vcs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 8516bd71..9cf84663 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -129,6 +129,8 @@ class Vcs(object): except subprocess.CalledProcessError: raise VcsError("{0:s} error on {1:s}. Command: {2:s}"\ .format(cmd, path, ' '.join([cmd] + args))) + except FileNotFoundError: + raise VcsError("{0:s} error on {1:s}: File not found".format(cmd, path)) # Generic #--------------------------- -- cgit 1.4.1-2-gfad0 From 431349cb771621fc336902695b724dfd193b924c Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 19 Oct 2015 22:26:57 +0200 Subject: VCS: Fix class for non-root; fixes action commands --- ranger/config/commands.py | 39 ++++++++++++++++++--------------------- ranger/ext/vcs/vcs.py | 1 + 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 9aee9f29..b2c704c7 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1336,18 +1336,16 @@ class stage(Command): def execute(self): from ranger.ext.vcs import VcsError - filelist = [f.path for f in self.fm.thistab.get_selection()] - self.fm.thisdir.vcs_outdated = True -# for f in self.fm.thistab.get_selection(): -# f.vcs_outdated = True - - try: - self.fm.thisdir.vcs.add(filelist) - except VcsError: - self.fm.notify("Could not stage files.") - - self.fm.reload_cwd() + if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: + filelist = [f.path for f in self.fm.thistab.get_selection()] + try: + self.fm.thisdir.vcs.add(filelist) + except VcsError as error: + self.fm.notify('Unable to unstage files: {0:s}'.format(str(error))) + self.fm.reload_cwd() + else: + self.fm.notify('Unable to stage files: Not in repository') class unstage(Command): """ @@ -1358,17 +1356,16 @@ class unstage(Command): def execute(self): from ranger.ext.vcs import VcsError - filelist = [f.path for f in self.fm.thistab.get_selection()] - self.fm.thisdir.vcs_outdated = True -# for f in self.fm.thistab.get_selection(): -# f.vcs_outdated = True - try: - self.fm.thisdir.vcs.reset(filelist) - except VcsError: - self.fm.notify("Could not unstage files.") - - self.fm.reload_cwd() + if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: + filelist = [f.path for f in self.fm.thistab.get_selection()] + try: + self.fm.thisdir.vcs.reset(filelist) + except VcsError as error: + self.fm.notify('Unable to unstage files: {0:s}'.format(str(error))) + self.fm.reload_cwd() + else: + self.fm.notify('Unable to unstage files: Not in repository') class diff(Command): diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 9cf84663..5e9f03c1 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -107,6 +107,7 @@ class Vcs(object): self.track = False self.in_repodir = True else: + self.__class__ = self.rootvcs.__class__ self.track = self.rootvcs.track self.in_repodir = False else: -- cgit 1.4.1-2-gfad0 From 70bf1625c2bffa60d952059c3f9d55c13014a588 Mon Sep 17 00:00:00 2001 From: nfnty Date: Wed, 21 Oct 2015 21:57:30 +0200 Subject: VCS: Implement symbolic link support --- ranger/container/directory.py | 2 +- ranger/ext/vcs/vcs.py | 101 ++++++++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 9725ae45..1b658063 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -338,7 +338,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): if item.vcs.is_root: self.has_vcschild = True else: - item.vcspathstatus = self.vcs.get_status_subpath( + item.vcspathstatus = item.vcs.get_status_subpath( item.path, is_directory=True) else: item = File(name, preload=stats, path_is_abs=True, diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 5e9f03c1..4bb3ee99 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -71,21 +71,29 @@ class Vcs(object): def __init__(self, directoryobject): self.obj = directoryobject - self.path = directoryobject.path + self.path = self.obj.path self.repotypes_settings = set( repotype for repotype, values in self.REPOTYPES.items() - if getattr(directoryobject.settings, values['setting']) in ('enabled', 'local') + if getattr(self.obj.settings, values['setting']) in ('enabled', 'local') ) - self.root, self.repodir, self.repotype = self.find_root(self.path) - self.is_root = True if self.path == self.root else False + self.in_repodir = False + self.track = False + + self.root, self.repodir, self.repotype, self.links = self.find_root(self.path) + self.is_root = True if self.obj.path == self.root else False if self.root: if self.is_root: + self.rootvcs = self self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), self.REPOTYPES[self.repotype]['class']) - self.rootvcs = self self.status_subpaths = {} - self.in_repodir = False + + if not os.access(self.repodir, os.R_OK): + directoryobject.vcspathstatus = 'unknown' + self.remotestatus = 'unknown' + return + try: self.head = self.get_info(self.HEAD) self.branch = self.get_branch() @@ -94,25 +102,18 @@ class Vcs(object): except VcsError: return - if os.access(self.repodir, os.R_OK): - self.track = True - else: - self.track = False - directoryobject.vcspathstatus = 'unknown' - self.remotestatus = 'unknown' + self.track = True else: self.rootvcs = directoryobject.fm.get_directory(self.root).vcs + self.rootvcs.links |= self.links + self.__class__ = self.rootvcs.__class__ + # Do not track self.repodir or its subpaths if self.path == self.repodir or self.path.startswith(self.repodir + '/'): - self.track = False self.in_repodir = True - else: - self.__class__ = self.rootvcs.__class__ - self.track = self.rootvcs.track - self.in_repodir = False - else: - self.track = False - self.in_repodir = False + return + + self.track = self.rootvcs.track # Auxiliar #--------------------------- @@ -146,14 +147,20 @@ class Vcs(object): def find_root(self, path): """Finds the repository root path. Otherwise returns none""" + links = set() while True: + if os.path.islink(path): + links.add(path) + relpath = os.path.relpath(self.path, path) + path = os.path.realpath(path) + self.path = os.path.normpath(os.path.join(path, relpath)) repodir, repotype = self.get_repotype(path) if repodir: - return (path, repodir, repotype) + return (path, repodir, repotype, links) if path == '/': break path = os.path.dirname(path) - return (None, None, None) + return (None, None, None, None) def check(self): """Check repository health""" @@ -162,7 +169,7 @@ class Vcs(object): self.__init__(self.obj) return True elif self.track and not os.path.exists(self.repodir): - self.update_tree(purge=True) + self.purge_tree() return False def update_root(self): @@ -174,11 +181,16 @@ class Vcs(object): self.rootvcs.remotestatus = self.rootvcs.get_status_remote() self.rootvcs.obj.vcspathstatus = self.rootvcs.get_status_root() except VcsError: - self.update_tree(purge=True) + self.purge_tree() - def update_tree(self, purge=False): - """Update repository tree""" - for wroot, wdirs, _ in os.walk(self.rootvcs.path): + def purge_tree(self): + """Purge tree""" + self.update_tree(purge=True) + self.rootvcs.__init__(self.rootvcs.obj) + + def _update_walk(self, path, purge): + """Update walk""" + for wroot, wdirs, _ in os.walk(path): # Only update loaded directories try: wroot_obj = self.obj.fm.directories[wroot] @@ -200,10 +212,10 @@ class Vcs(object): if not fileobj.vcs.track: continue if not fileobj.vcs.is_root: - fileobj.vcspathstatus = self.rootvcs.get_status_subpath( + fileobj.vcspathstatus = wroot_obj.vcs.get_status_subpath( fileobj.path, is_directory=True) else: - fileobj.vcspathstatus = self.rootvcs.get_status_subpath(fileobj.path) + fileobj.vcspathstatus = wroot_obj.vcs.get_status_subpath(fileobj.path) # Remove dead directories for wdir in wdirs.copy(): @@ -214,8 +226,21 @@ class Vcs(object): continue if wdir_obj.vcs.is_root or not wdir_obj.vcs.track: wdirs.remove(wdir) - if purge: - self.rootvcs.__init__(self.rootvcs.obj) + + def update_tree(self, purge=False): + """Update tree""" + self._update_walk(self.root, purge) + for path in self.rootvcs.links: + self._update_walk(path, purge) + try: + fileobj = self.obj.fm.directories[path] + except KeyError: + continue + if fileobj.vcs.path == self.root: + fileobj.vcspathstatus = self.rootvcs.get_status_root() + else: + fileobj.vcspathstatus = fileobj.vcs.get_status_subpath( + fileobj.path, is_directory=True) # Repo creation #--------------------------- @@ -303,8 +328,18 @@ class Vcs(object): return 'sync' def get_status_subpath(self, path, is_directory=False): - """Returns the status of path""" - relpath = os.path.relpath(path, self.root) + """ + Returns the status of path + + path needs to be self.obj.path or subpath thereof + """ + if path == self.obj.path: + relpath = os.path.relpath(self.path, self.root) + else: + relpath = os.path.relpath( + os.path.join(self.path, os.path.relpath(path, self.obj.path)), + self.root, + ) # check if relpath or its parents has a status tmppath = relpath -- cgit 1.4.1-2-gfad0 From afa5d293a11f2955871493ad25b161beddad96a6 Mon Sep 17 00:00:00 2001 From: nfnty Date: Wed, 21 Oct 2015 22:40:01 +0200 Subject: VCS: Minor fixes --- ranger/gui/widgets/browsercolumn.py | 13 ++++++++----- ranger/gui/widgets/browserview.py | 3 --- ranger/gui/widgets/statusbar.py | 3 --- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 8961f48f..13c8f510 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -256,11 +256,14 @@ class BrowserColumn(Pager): metakey = hash(repr(sorted(metadata.items()))) if metadata else 0 key = (self.wid, selected_i == i, drawn.marked, self.main_column, - drawn.path in copied, tagged_marker, drawn.infostring, - drawn.vcspathstatus, - drawn.vcs.remotestatus if drawn.is_directory and drawn.vcs and drawn.vcs.track and drawn.vcs.is_root else None, - self.fm.do_cut, - current_linemode.name, metakey) + drawn.path in copied, tagged_marker, drawn.infostring, + drawn.vcspathstatus, + drawn.vcs.remotestatus \ + if drawn.is_directory and drawn.vcs \ + and drawn.vcs.is_root and drawn.vcs.track \ + else None, + self.fm.do_cut, + current_linemode.name, metakey) if key in drawn.display_data: self.execute_curses_batch(line, drawn.display_data[key]) diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index 56b9eba7..e9640208 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -21,7 +21,6 @@ class BrowserView(Widget, DisplayableContainer): old_collapse = False draw_hints = False draw_info = False - vcsthread = None def __init__(self, win, ratios, preview = True): DisplayableContainer.__init__(self, win) @@ -92,8 +91,6 @@ class BrowserView(Widget, DisplayableContainer): self.need_redraw = True self.need_clear = False for tab in self.fm.tabs.values(): - if tab == self.fm.thistab: - continue directory = tab.thisdir if directory: directory.load_content_if_outdated() diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 538757fd..9783a7ad 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -93,9 +93,6 @@ class StatusBar(Widget): self.old_ctime = ctime self.need_redraw = True - if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: - self.need_redraw = True - if self.need_redraw: self.need_redraw = False -- cgit 1.4.1-2-gfad0 From 3dff9d73d7fd225f77c18f5a53b3f804d7929e98 Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 22 Oct 2015 07:41:03 +0200 Subject: VCS: Rename vcslocal to vcsnone --- ranger/colorschemes/default.py | 2 +- ranger/gui/context.py | 2 +- ranger/gui/widgets/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index 7a248f2e..967fa50c 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -137,7 +137,7 @@ class Default(ColorScheme): elif context.vcsremote and not context.selected: attr &= ~bold - if context.vcssync or context.vcslocal: + if context.vcssync or context.vcsnone: fg = green elif context.vcsbehind: fg = red diff --git a/ranger/gui/context.py b/ranger/gui/context.py index 66d43d1a..6b341bf1 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -19,7 +19,7 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo', 'infostring', 'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit', 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', - 'vcsstaged', 'vcssync', 'vcslocal', 'vcsbehind', 'vcsahead', 'vcsdiverged'] + 'vcsstaged', 'vcssync', 'vcsnone', 'vcsbehind', 'vcsahead', 'vcsdiverged'] class Context(object): def __init__(self, keys): diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index 4dd28c6d..200ae8a8 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -15,7 +15,7 @@ class Widget(Displayable): 'none': (' ', []), 'unknown': ('?', ["vcsunknown"])} - vcsremotestatus_symb = {'none': ('⌂', ["vcslocal"]), + vcsremotestatus_symb = {'none': ('⌂', ["vcsnone"]), 'sync': ('=', ["vcssync"]), 'behind': ('<', ["vcsbehind"]), 'ahead': ('>', ["vcsahead"]), -- cgit 1.4.1-2-gfad0 From 94fd7bb80e28e5566583c82b9f05ece042349a2c Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 22 Oct 2015 07:47:34 +0200 Subject: VCS: Fix symlinks pointing to root, Cleanup --- ranger/container/directory.py | 2 ++ ranger/ext/vcs/bzr.py | 3 -- ranger/ext/vcs/git.py | 30 ++++++++----------- ranger/ext/vcs/hg.py | 4 --- ranger/ext/vcs/svn.py | 4 --- ranger/ext/vcs/vcs.py | 70 ++++++++----------------------------------- 6 files changed, 27 insertions(+), 86 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 1b658063..c96633b9 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -337,6 +337,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): if item.vcs and item.vcs.track: if item.vcs.is_root: self.has_vcschild = True + elif item.is_link and os.path.realpath(item.path) == item.vcs.root: + item.vcspathstatus = item.vcs.rootvcs.get_status_root() else: item.vcspathstatus = item.vcs.get_status_subpath( item.path, is_directory=True) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 7b457997..8e9f44d3 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. # Author: Abdó Roig-Maranges , 2012 @@ -265,5 +264,3 @@ class Bzr(Vcs): return raw.split('\n') else: return [] - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 214fe63e..b7c51fa6 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. # Author: Abdó Roig-Maranges , 2011-2012 @@ -268,20 +267,19 @@ class Git(Vcs): def get_remote(self): """Returns the url for the remote repo attached to head""" - if self.is_repo(): - try: - ref = self._head_ref() - remote = self._remote_ref(ref) - except VcsError: - ref = remote = None - if not remote: - return None - - match = re.match('refs/remotes/([^/]+)/', remote) - if match: - return self._git(['config', '--get', 'remote.{0:s}.url'.format(match.group(1))], - catchout=True).strip() \ - or None + try: + ref = self._head_ref() + remote = self._remote_ref(ref) + except VcsError: + ref = remote = None + if not remote: + return None + + match = re.match('refs/remotes/([^/]+)/', remote) + if match: + return self._git(['config', '--get', 'remote.{0:s}.url'.format(match.group(1))], + catchout=True).strip() \ + or None return None @@ -325,5 +323,3 @@ class Git(Vcs): else: return self._git(['ls-tree', '--name-only', '-r', '-z', rev], catchout=True, bytes=True).decode('utf-8').split('\x00') - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index f91d18cc..77587408 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. # Author: Abdó Roig-Maranges , 2011-2012 @@ -265,6 +264,3 @@ class Hg(Vcs): return raw.split('\n') else: return [] - - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 64a690c1..9868b5fd 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. # Author: Abdó Roig-Maranges , 2011-2012 @@ -268,6 +267,3 @@ class SVN(Vcs): raw = self._svn(self.path, ['ls', "-r", rev], catchout=True) return raw.split('\n') - - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 4bb3ee99..6cb4809d 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. # Author: Abdó Roig-Maranges , 2011-2012 @@ -47,7 +46,7 @@ class Vcs(object): 'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn'}, } - # Possible status responses in order of importance with statuses that + # Possible directory statuses in order of importance with statuses that # don't make sense disabled DIR_STATUS = ( 'conflict', @@ -60,14 +59,6 @@ class Vcs(object): # 'none', 'unknown', ) - # REMOTE_STATUS = ( - # 'diverged', - # 'behind', - # 'ahead', - # 'sync', - # 'none', - # 'unknown', - # ) def __init__(self, directoryobject): self.obj = directoryobject @@ -230,48 +221,19 @@ class Vcs(object): def update_tree(self, purge=False): """Update tree""" self._update_walk(self.root, purge) - for path in self.rootvcs.links: + for path in self.rootvcs.links.copy(): self._update_walk(path, purge) try: - fileobj = self.obj.fm.directories[path] + dirobj = self.obj.fm.directories[path] except KeyError: continue - if fileobj.vcs.path == self.root: - fileobj.vcspathstatus = self.rootvcs.get_status_root() + if purge: + dirobj.vcspathstatus = None + elif dirobj.vcs.path == self.root: + dirobj.vcspathstatus = self.rootvcs.get_status_root() else: - fileobj.vcspathstatus = fileobj.vcs.get_status_subpath( - fileobj.path, is_directory=True) - - # Repo creation - #--------------------------- - - def init(self, repotype): - """Initializes a repo in current path""" - if not repotype in self.repo_types: - raise VcsError("Unrecognized repo type {0:s}".format(repotype)) - - if not os.path.exists(self.path): - os.makedirs(self.path) - try: - self.__class__ = self.repo_types[repotype] - self.init() - except: - self.__class__ = Vcs - raise - - def clone(self, repotype, src): - """Clones a repo from src""" - if not repotype in self.repo_types: - raise VcsError("Unrecognized repo type {0:s}".format(repotype)) - - if not os.path.exists(self.path): - os.makedirs(self.path) - try: - self.__class__ = self.repo_types[repotype] - self.clone(src) - except: - self.__class__ = Vcs - raise + dirobj.vcspathstatus = dirobj.vcs.get_status_subpath( + dirobj.path, is_directory=True) # Action interface #--------------------------- @@ -307,16 +269,8 @@ class Vcs(object): # Data #--------------------------- - def is_repo(self): - """Checks wether there is an initialized repo in self.path""" - return self.path and os.path.exists(self.path) and self.root is not None - - def is_tracking(self): - """Checks whether HEAD is tracking a remote repo""" - return self.get_remote(self.HEAD) is not None - def get_status_root_cheap(self): - """Returns the status of a child root, very cheap""" + """Returns the status of self.root, very cheap""" raise NotImplementedError def get_status_root(self): @@ -359,7 +313,7 @@ class Vcs(object): def get_status_subpaths(self): """Returns a dict indexed by subpaths not in sync their status as values. - Paths are given relative to the root. Strips trailing '/' from dirs.""" + Paths are given relative to self.root. Strips trailing '/' from dirs.""" raise NotImplementedError def get_status_remote(self): @@ -437,8 +391,8 @@ class VcsThread(threading.Thread): self.ui.redraw() roots.clear() - self.wake.clear() self.wake.wait(timeout=self.delay) + self.wake.clear() def wakeup(self): """Wakeup thread""" -- cgit 1.4.1-2-gfad0 From 5741db319e4f7a78045cfcc6cb0ac95919bc0a4c Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 22 Oct 2015 08:55:25 +0200 Subject: VCS: Fix flat: Only initialize subdir once --- ranger/container/directory.py | 19 +++++++++++-------- ranger/container/fsobject.py | 6 +----- ranger/ext/vcs/vcs.py | 15 ++++++++++++++- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index c96633b9..a31d4ec9 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -323,17 +323,20 @@ class Directory(FileSystemObject, Accumulator, Loadable): stats = None is_a_dir = False if is_a_dir: - if self.flat: + try: + item = self.fm.get_directory(name) + item.load_if_outdated() + except: item = Directory(name, preload=stats, path_is_abs=True, - basename_is_rel_to=basename_is_rel_to) + basename_is_rel_to=basename_is_rel_to) item.load() else: - try: - item = self.fm.get_directory(name) - item.load_if_outdated() - except: - item = Directory(name, preload=stats, path_is_abs=True) - item.load() + if self.flat: + item.relative_path = os.path.relpath(item.path, self.path) + else: + item.relative_path = item.basename + item.relative_path_lower = item.relative_path.lower() + if item.vcs and item.vcs.track: if item.vcs.is_root: self.has_vcschild = True diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index 1a33f1c4..c2a8004c 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -75,8 +75,6 @@ class FileSystemObject(FileManagerAware, SettingsAware): vcspathstatus = None - basename_is_rel_to = None - _linemode = DEFAULT_LINEMODE linemode_dict = dict( (linemode.name, linemode()) for linemode in @@ -87,12 +85,10 @@ class FileSystemObject(FileManagerAware, SettingsAware): if not path_is_abs: path = abspath(path) self.path = path - self.basename_is_rel_to = basename_is_rel_to + self.basename = basename(path) if basename_is_rel_to == None: - self.basename = basename(path) self.relative_path = self.basename else: - self.basename = basename(path) self.relative_path = relpath(path, basename_is_rel_to) self.relative_path_lower = self.relative_path.lower() self.extension = splitext(self.basename)[1].lstrip(extsep) or None diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 6cb4809d..2815916a 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -360,11 +360,23 @@ class VcsThread(threading.Thread): self.delay = idle_delay / 1000 self.wake = threading.Event() + def _is_flat(self): + """Check for flat mode""" + for column in self.ui.browser.columns: + if column.target and column.target.is_directory and column.target.flat: + return True + return False + def run(self): # Set for already updated roots roots = set() redraw = False while True: + if self._is_flat(): + self.wake.wait(timeout=self.delay) + self.wake.clear() + continue + for column in self.ui.browser.columns: target = column.target if target and target.is_directory and target.vcs: @@ -382,6 +394,7 @@ class VcsThread(threading.Thread): target.vcs.update_root() target.vcs.update_tree() redraw = True + if redraw: redraw = False for column in self.ui.browser.columns: @@ -389,8 +402,8 @@ class VcsThread(threading.Thread): column.need_redraw = True self.ui.status.need_redraw = True self.ui.redraw() - roots.clear() + roots.clear() self.wake.wait(timeout=self.delay) self.wake.clear() -- cgit 1.4.1-2-gfad0 From cf750b5b890d3dd903116459eb621fae1875a5c6 Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 23 Oct 2015 01:05:57 +0200 Subject: VCS: Only redraw when awoken --- ranger/ext/vcs/vcs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 2815916a..ca16fbbf 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -360,19 +360,18 @@ class VcsThread(threading.Thread): self.delay = idle_delay / 1000 self.wake = threading.Event() - def _is_flat(self): - """Check for flat mode""" + def _check(self): + """Check for hinders""" for column in self.ui.browser.columns: if column.target and column.target.is_directory and column.target.flat: return True return False def run(self): - # Set for already updated roots - roots = set() + roots = set() # already updated roots redraw = False while True: - if self._is_flat(): + if self._check(): self.wake.wait(timeout=self.delay) self.wake.clear() continue @@ -401,11 +400,12 @@ class VcsThread(threading.Thread): if column.target and column.target.is_directory: column.need_redraw = True self.ui.status.need_redraw = True - self.ui.redraw() + if self.wake.is_set(): + self.ui.redraw() roots.clear() - self.wake.wait(timeout=self.delay) self.wake.clear() + self.wake.wait(timeout=self.delay) def wakeup(self): """Wakeup thread""" -- cgit 1.4.1-2-gfad0 From 87e473e0921f05b5e66f84d5bdc9e810a65218cc Mon Sep 17 00:00:00 2001 From: nfnty Date: Tue, 27 Oct 2015 03:03:12 +0100 Subject: VCS: Fix repo deletion race --- ranger/ext/vcs/vcs.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index ca16fbbf..a6d12db7 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -158,10 +158,10 @@ class Vcs(object): if not self.in_repodir \ and (not self.track or (not self.is_root and self.get_repotype(self.path)[0])): self.__init__(self.obj) - return True elif self.track and not os.path.exists(self.repodir): - self.purge_tree() + self.update_tree(purge=True) return False + return True def update_root(self): """Update repository""" @@ -172,12 +172,9 @@ class Vcs(object): self.rootvcs.remotestatus = self.rootvcs.get_status_remote() self.rootvcs.obj.vcspathstatus = self.rootvcs.get_status_root() except VcsError: - self.purge_tree() - - def purge_tree(self): - """Purge tree""" - self.update_tree(purge=True) - self.rootvcs.__init__(self.rootvcs.obj) + self.update_tree(purge=True) + return False + return True def _update_walk(self, path, purge): """Update walk""" @@ -229,11 +226,14 @@ class Vcs(object): continue if purge: dirobj.vcspathstatus = None + dirobj.vcs.__init__(dirobj.vcs.obj) elif dirobj.vcs.path == self.root: dirobj.vcspathstatus = self.rootvcs.get_status_root() else: dirobj.vcspathstatus = dirobj.vcs.get_status_subpath( dirobj.path, is_directory=True) + if purge: + self.rootvcs.__init__(self.rootvcs.obj) # Action interface #--------------------------- @@ -390,9 +390,9 @@ class VcsThread(threading.Thread): and (clmn.target.path == target.vcs.repodir or clmn.target.path.startswith(target.vcs.repodir + '/'))): continue - target.vcs.update_root() - target.vcs.update_tree() - redraw = True + if target.vcs.update_root(): + target.vcs.update_tree() + redraw = True if redraw: redraw = False -- cgit 1.4.1-2-gfad0 From 5b14517afec332127610a087f0717f7f96b96b37 Mon Sep 17 00:00:00 2001 From: nfnty Date: Wed, 28 Oct 2015 04:29:47 +0100 Subject: VCS: Fix python2 compatibility --- AUTHORS | 2 ++ ranger/ext/vcs/bzr.py | 6 +----- ranger/ext/vcs/git.py | 6 +----- ranger/ext/vcs/hg.py | 6 +----- ranger/ext/vcs/svn.py | 9 +-------- ranger/ext/vcs/vcs.py | 39 ++++++++++++++++++--------------------- 6 files changed, 24 insertions(+), 44 deletions(-) diff --git a/AUTHORS b/AUTHORS index ec17affb..d3789119 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,6 +21,8 @@ Copyright 2015 Delisa Mason Copyright 2015 No Suck Copyright 2015 Randy Nance Copyright 2015 Wojciech Siewierski +Copyright 2015 Ryan Burns +Copyright 2015 nfnty Ideally, all contributors of non-trivial code are named here to the extent that a name and e-mail address is available. Please write a mail to hut@hut.pm if diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 8e9f44d3..7b870d7d 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -1,8 +1,4 @@ -# This file is part of ranger, the console file manager. -# License: GNU GPL version 3, see the file "AUTHORS" for details. -# Author: Abdó Roig-Maranges , 2012 -# -# vcs - a python module to handle various version control systems +"""GNU Bazaar module""" import os import re diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index b7c51fa6..ab971423 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -1,8 +1,4 @@ -# This file is part of ranger, the console file manager. -# License: GNU GPL version 3, see the file "AUTHORS" for details. -# Author: Abdó Roig-Maranges , 2011-2012 -# -# vcs - a python module to handle various version control systems +"""Git module""" import os import re diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 77587408..48e991bf 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -1,8 +1,4 @@ -# This file is part of ranger, the console file manager. -# License: GNU GPL version 3, see the file "AUTHORS" for details. -# Author: Abdó Roig-Maranges , 2011-2012 -# -# vcs - a python module to handle various version control systems +"""Mercurial module""" import os import re diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 9868b5fd..f7e4bbf4 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -1,11 +1,4 @@ -# This file is part of ranger, the console file manager. -# License: GNU GPL version 3, see the file "AUTHORS" for details. -# Author: Abdó Roig-Maranges , 2011-2012 -# Ryan Burns , 2015 -# -# R. Burns start with Abdó's Hg module and modified it for Subversion. -# -# vcs - a python module to handle various version control systems +"""Subversion module""" from __future__ import with_statement import os diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index a6d12db7..2ea2773e 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -1,9 +1,4 @@ -# This file is part of ranger, the console file manager. -# License: GNU GPL version 3, see the file "AUTHORS" for details. -# Author: Abdó Roig-Maranges , 2011-2012 -# -# vcs - a python module to handle various version control systems -"""Vcs module""" +"""VCS module""" import os import subprocess @@ -14,22 +9,23 @@ class VcsError(Exception): pass class Vcs(object): - """ This class represents a version controlled path, abstracting the usual - operations from the different supported backends. + """ + This class represents a version controlled path, abstracting the usual + operations from the different supported backends. - The backends are declared in te variable self.repo_types, and are derived - classes from Vcs with the following restrictions: + The backends are declared in te variable self.repo_types, and are derived + classes from Vcs with the following restrictions: - * do NOT implement __init__. Vcs takes care of this. + * do NOT implement __init__. Vcs takes care of this. - * do not create change internal state. All internal state should be - handled in Vcs + * do not create change internal state. All internal state should be + handled in Vcs - Objects from backend classes should never be created directly. Instead - create objects of Vcs class. The initialization calls update, which takes - care of detecting the right Vcs backend to use and dynamically changes the - object type accordingly. - """ + Objects from backend classes should never be created directly. Instead + create objects of Vcs class. The initialization calls update, which takes + care of detecting the right Vcs backend to use and dynamically changes the + object type accordingly. + """ # These are abstracted revs, representing the current index (staged files), # the current head and nothing. Every backend should redefine them if the @@ -206,7 +202,7 @@ class Vcs(object): fileobj.vcspathstatus = wroot_obj.vcs.get_status_subpath(fileobj.path) # Remove dead directories - for wdir in wdirs.copy(): + for wdir in list(wdirs): try: wdir_obj = self.obj.fm.directories[os.path.join(wroot, wdir)] except KeyError: @@ -218,7 +214,7 @@ class Vcs(object): def update_tree(self, purge=False): """Update tree""" self._update_walk(self.root, purge) - for path in self.rootvcs.links.copy(): + for path in list(self.rootvcs.links): self._update_walk(path, purge) try: dirobj = self.obj.fm.directories[path] @@ -355,7 +351,8 @@ class Vcs(object): class VcsThread(threading.Thread): """Vcs thread""" def __init__(self, ui, idle_delay): - super(VcsThread, self).__init__(daemon=True) + super(VcsThread, self).__init__() + self.daemon = True self.ui = ui self.delay = idle_delay / 1000 self.wake = threading.Event() -- cgit 1.4.1-2-gfad0 From f24d97cc4dad56dd409bd1798b1c26d9a986ee63 Mon Sep 17 00:00:00 2001 From: nfnty Date: Tue, 10 Nov 2015 00:59:15 +0100 Subject: VCS: Delay a minimum of 1 second --- ranger/ext/vcs/vcs.py | 2 +- ranger/gui/ui.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 2ea2773e..241038e0 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -354,7 +354,7 @@ class VcsThread(threading.Thread): super(VcsThread, self).__init__() self.daemon = True self.ui = ui - self.delay = idle_delay / 1000 + self.delay = idle_delay self.wake = threading.Event() def _check(self): diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 49c99777..c3f68586 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -253,7 +253,7 @@ class UI(DisplayableContainer): self.add_child(self.pager) # Create VCS thread - self.vcsthread = VcsThread(self, self.settings.idle_delay) + self.vcsthread = VcsThread(self, max(1, self.settings.idle_delay / 1000)) self.vcsthread.start() def redraw(self): -- cgit 1.4.1-2-gfad0 From 34fec205ee153928b7b99d888f04be758041beed Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 18 Dec 2015 01:47:21 +0100 Subject: VCS: Major cleanup --- ranger/config/commands.py | 106 ++++++++---------------------- ranger/ext/vcs/__init__.py | 2 +- ranger/ext/vcs/bzr.py | 154 ++------------------------------------------ ranger/ext/vcs/git.py | 157 ++++++--------------------------------------- ranger/ext/vcs/hg.py | 128 +----------------------------------- ranger/ext/vcs/svn.py | 128 +----------------------------------- ranger/ext/vcs/vcs.py | 137 +++++++++++++-------------------------- 7 files changed, 100 insertions(+), 712 deletions(-) diff --git a/ranger/config/commands.py b/ranger/config/commands.py index b2c704c7..6012ed4e 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1324,9 +1324,31 @@ class grep(Command): action.extend(f.path for f in self.fm.thistab.get_selection()) self.fm.execute_command(action, flags='p') +class flat(Command): + """ + :flat + + Flattens the directory view up to the specified level. + + -1 fully flattened + 0 remove flattened view + """ + + def execute(self): + try: + level = self.rest(1) + level = int(level) + except ValueError: + level = self.quantifier + if level < -1: + self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True) + self.fm.thisdir.unload() + self.fm.thisdir.flat = level + self.fm.thisdir.load_content() # Version control commands # -------------------------------- + class stage(Command): """ :stage @@ -1336,14 +1358,13 @@ class stage(Command): def execute(self): from ranger.ext.vcs import VcsError - if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: filelist = [f.path for f in self.fm.thistab.get_selection()] try: self.fm.thisdir.vcs.add(filelist) except VcsError as error: - self.fm.notify('Unable to unstage files: {0:s}'.format(str(error))) - self.fm.reload_cwd() + self.fm.notify('Unable to stage files: {0:s}'.format(str(error))) + self.fm.ui.vcsthread.wakeup() else: self.fm.notify('Unable to stage files: Not in repository') @@ -1356,93 +1377,16 @@ class unstage(Command): def execute(self): from ranger.ext.vcs import VcsError - if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: filelist = [f.path for f in self.fm.thistab.get_selection()] try: self.fm.thisdir.vcs.reset(filelist) except VcsError as error: self.fm.notify('Unable to unstage files: {0:s}'.format(str(error))) - self.fm.reload_cwd() + self.fm.ui.vcsthread.wakeup() else: self.fm.notify('Unable to unstage files: Not in repository') - -class diff(Command): - """ - :diff - - Displays a diff of selected files against the last committed version - """ - def execute(self): - from ranger.ext.vcs import VcsError - import tempfile - - L = self.fm.thistab.get_selection() - if len(L) == 0: return - - filelist = [f.path for f in L] - vcs = L[0].vcs - - diff = vcs.get_raw_diff(filelist=filelist) - if len(diff.strip()) > 0: - tmp = tempfile.NamedTemporaryFile() - tmp.write(diff.encode('utf-8')) - tmp.flush() - - pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER) - self.fm.run([pager, tmp.name]) - else: - raise Exception("diff is empty") - - -class log(Command): - """ - :log - - Displays the log of the current repo or files - """ - def execute(self): - from ranger.ext.vcs import VcsError - import tempfile - - L = self.fm.thistab.get_selection() - if len(L) == 0: return - - filelist = [f.path for f in L] - vcs = L[0].vcs - - log = vcs.get_raw_log(filelist=filelist) - tmp = tempfile.NamedTemporaryFile() - tmp.write(log.encode('utf-8')) - tmp.flush() - - pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER) - self.fm.run([pager, tmp.name]) - -class flat(Command): - """ - :flat - - Flattens the directory view up to the specified level. - - -1 fully flattened - 0 remove flattened view - """ - - def execute(self): - try: - level = self.rest(1) - level = int(level) - except ValueError: - level = self.quantifier - if level < -1: - self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True) - self.fm.thisdir.unload() - self.fm.thisdir.flat = level - self.fm.thisdir.load_content() - - # Metadata commands # -------------------------------- diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index c6c691b8..3e62310c 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -1,3 +1,3 @@ -"""VCS Extension Package""" +"""VCS Extension""" from .vcs import Vcs, VcsError, VcsThread diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 7b870d7d..358db8ad 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -2,28 +2,25 @@ import os import re -import shutil from datetime import datetime from .vcs import Vcs, VcsError - class Bzr(Vcs): + """VCS implementiation for Bzr""" HEAD="last:1" - # Auxiliar stuff + # Generic #--------------------------- def _bzr(self, path, args, silent=True, catchout=False, bytes=False): return self._vcs(path, 'bzr', args, silent=silent, catchout=catchout, bytes=bytes) - 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() @@ -31,7 +28,6 @@ class Bzr(Vcs): return rev - 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'] @@ -61,7 +57,6 @@ class Bzr(Vcs): log.append(dt) return log - def _bzr_file_status(self, st): st = st.strip() if st in "AM": return 'staged' @@ -69,76 +64,19 @@ class Bzr(Vcs): elif st in "?": return 'untracked' else: return 'unknown' - - - # Repo creation - #--------------------------- - - def init(self): - """Initializes a repo in current path""" - self._bzr(self.path, ['init']) - self.update() - - - def clone(self, src): - """Clones a repo from src""" - path = os.path.dirname(self.path) - name = os.path.basename(self.path) - try: - os.rmdir(self.path) - except OSError: - raise VcsError("Can't clone to %s. It is not an empty directory" % self.path) - - self._bzr(path, ['branch', src, name]) - self.update() - - - # Action Interface #--------------------------- - def commit(self, message): - """Commits with a given message""" - self._bzr(self.path, ['commit', '-m', message]) - - def add(self, filelist=None): """Adds files to the index, preparing for commit""" if filelist != None: self._bzr(self.path, ['add'] + filelist) else: self._bzr(self.path, ['add']) - def reset(self, filelist=None): """Removes files from the index""" if filelist != None: self._bzr(self.path, ['remove', '--keep', '--new'] + filelist) else: self._bzr(self.path, ['remove', '--keep', '--new']) - - def pull(self): - """Pulls a git repo""" - self._bzr(self.path, ['pull']) - - - def push(self): - """Pushes a git repo""" - self._bzr(self.path, ['push']) - - - def checkout(self, rev): - """Checks out a branch or revision""" - self._bzr(self.path, ['update', '-r', rev]) - - - def extract_file(self, rev, name, dest): - """Extracts a file from a given revision and stores it in dest dir""" - if rev == self.INDEX: - shutil.copyfile(os.path.join(self.path, name), dest) - else: - out = self._bzr(self.path, ['cat', '--r', rev, name], catchout=True, bytes=True) - with open(dest, 'wb') as fd: fd.write(out) - - - # Data Interface #--------------------------- @@ -153,65 +91,7 @@ class Bzr(Vcs): ret[os.path.normpath(p.strip())] = sta return ret - - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo. Strips trailing '/' from dirs.""" - raw = self._bzr(self.path, ['ls', '--ignored'], catchout=True) - return set(os.path.normpath(p) for p in raw.split('\n')) - - - # TODO: slow due to net access def get_status_remote(self): - """Checks the status of the repo regarding sync state with remote branch""" - if self.get_remote() == None: - return "none" - - ahead = behind = True - try: - self._bzr(self.path, ['missing', '--mine-only'], silent=True) - except: - ahead = False - - try: - self._bzr(self.path, ['missing', '--theirs-only'], silent=True) - except: - behind = False - - if ahead and behind: return "diverged" - elif ahead and not behind: return "ahead" - elif not ahead and behind: return "behind" - elif not ahead and not behind: return "sync" - - - def get_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" - branch = self._bzr(self.path, ['nick'], catchout=True) - return branch or None - - - def get_log(self, filelist=None, maxres=None): - """Get the entire log for the current HEAD""" - if not self._has_head(): return [] - return self._log(refspec=None, filelist=filelist) - - - def get_raw_log(self, filelist=None): - """Gets the raw log as a string""" - if not self._has_head(): return [] - args = ['log'] - if filelist: args = args + filelist - return self._bzr(self.path, args, catchout=True) - - - def get_raw_diff(self, refspec=None, filelist=None): - """Gets the raw diff as a string""" - args = ['diff', '--git'] - if refspec: args = args + [refspec] - if filelist: args = args + filelist - return self._bzr(self.path, args, catchout=True) - - - def get_remote(self): """Returns the url for the remote repo attached to head""" try: remote = self._bzr(self.path, ['config', 'parent_location'], catchout=True) @@ -220,19 +100,10 @@ class Bzr(Vcs): return remote.strip() or None - - def get_revision_id(self, rev=None): - """Get a canonical key for the revision rev""" - if rev == None: rev = self.HEAD - elif rev == self.INDEX: return None - rev = self._sanitize_rev(rev) - try: - L = self._log(refspec=rev) - except VcsError: - L = [] - if len(L) == 0: return None - else: return L[0]['revid'] - + def get_branch(self): + """Returns the current named branch, if this makes sense for the backend. None otherwise""" + branch = self._bzr(self.path, ['nick'], catchout=True) + return branch or None def get_info(self, rev=None): """Gets info about the given revision rev""" @@ -247,16 +118,3 @@ class Bzr(Vcs): raise VcsError("More than one instance of revision %s ?!?" % rev) else: return L[0] - - - def get_files(self, rev=None): - """Gets a list of files in revision rev""" - if rev == None: rev = self.HEAD - rev = self._sanitize_rev(rev) - - if rev: - if rev == self.INDEX: raw = self._bzr(self.path, ["ls"], catchout=True) - else: raw = self._bzr(self.path, ['ls', '--R', '-V', '-r', rev], catchout=True) - return raw.split('\n') - else: - return [] diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index ab971423..ab7f516f 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -2,7 +2,6 @@ import os import re -import shutil from datetime import datetime import json @@ -23,22 +22,14 @@ class Git(Vcs): ('!', '!', 'ignored'), ) - # Auxiliar stuff + # Generic #--------------------------- def _git(self, args, path=None, silent=True, catchout=False, bytes=False): """Call git""" - return self._vcs(path if path else self.path, 'git', args, silent=silent, + return self._vcs(path or self.path, 'git', args, silent=silent, catchout=catchout, bytes=bytes) - def _has_head(self): - """Checks whether repo has head""" - try: - self._git(['rev-parse', 'HEAD'], silent=True) - except VcsError: - return False - return True - def _head_ref(self): """Gets HEAD's ref""" return self._git(['symbolic-ref', self.HEAD], catchout=True, silent=True) or None @@ -51,17 +42,15 @@ class Git(Vcs): catchout=True, silent=True) \ or None - def _sanitize_rev(self, rev): - """Sanitize revision string""" - if rev is None: - return None - return rev.strip() - def _log(self, refspec=None, maxres=None, filelist=None): """Gets a list of dicts containing revision info, for the revisions matching refspec""" args = [ '--no-pager', 'log', - '--pretty={%x00short%x00: %x00%h%x00, %x00revid%x00: %x00%H%x00, %x00author%x00: %x00%an <%ae>%x00, %x00date%x00: %ct, %x00summary%x00: %x00%s%x00}' + '--pretty={' + '%x00short%x00:%x00%h%x00,' + '%x00revid%x00: %x00%H%x00,' + '%x00author%x00: %x00%an <%ae>%x00, %x00date%x00: %ct, %x00summary%x00: %x00%s%x00' + '}' ] if refspec: args += ['-1', refspec] @@ -70,9 +59,14 @@ class Git(Vcs): if filelist: args += ['--'] + filelist + try: + log_raw = self._git(args, catchout=True)\ + .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines() + except VcsError: + return [] + log = [] - for line in self._git(args, catchout=True)\ - .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines(): + for line in log_raw: line = json.loads(line) line['date'] = datetime.fromtimestamp(line['date']) log.append(line) @@ -85,31 +79,9 @@ class Git(Vcs): return status return 'unknown' - # Repo creation - #--------------------------- - - def init(self): - """Initializes a repo in current path""" - self._git(['init']) - self.update() - - def clone(self, src): - """Clones a repo from src""" - try: - os.rmdir(self.path) - except OSError: - raise VcsError("Can't clone to {0:s}: Not an empty directory".format(self.path)) - - self._git(['clone', src, os.path.basename(self.path)], path=os.path.dirname(self.path)) - self.update() - # Action interface #--------------------------- - def commit(self, message): - """Commits with a given message""" - self._git(['commit', '--message', message]) - def add(self, filelist=None): """Adds files to the index, preparing for commit""" if filelist: @@ -124,30 +96,6 @@ class Git(Vcs): else: self._git(['reset']) - def pull(self, *args): - """Pulls from remote""" - self._git(['pull'] + list(args)) - - def push(self, *args): - """Pushes to remote""" - self._git(['push'] + list(args)) - - def checkout(self, rev): - """Checks out a branch or revision""" - self._git(['checkout', self._sanitize_rev(rev)]) - - def extract_file(self, rev, name, dest): - """Extracts a file from a given revision and stores it in dest dir""" - if rev == self.INDEX: - shutil.copyfile(os.path.join(self.path, name), dest) - else: - with open(dest, 'wb') as fd: - fd.write( - self._git([ - '--no-pager', 'show', '{0:s}:{1:s}'.format(self._sanitize_rev(rev), name) - ], catchout=True, bytes=True) - ) - # Data Interface #--------------------------- @@ -237,85 +185,18 @@ class Git(Vcs): else: return None - def get_log(self, filelist=None, maxres=None): - """Get the entire log for the current HEAD""" - if not self._has_head(): - return [] - return self._log(refspec=None, maxres=maxres, filelist=filelist) - - def get_raw_log(self, filelist=None): - """Gets the raw log as a string""" - if not self._has_head(): - return [] - args = ['log'] - if filelist: - args += ['--'] + filelist - return self._git(args, catchout=True) - - def get_raw_diff(self, refspec=None, filelist=None): - """Gets the raw diff as a string""" - args = ['diff'] - if refspec: - args += [refspec] - if filelist: - args += ['--'] + filelist - return self._git(args, catchout=True) - - def get_remote(self): - """Returns the url for the remote repo attached to head""" - try: - ref = self._head_ref() - remote = self._remote_ref(ref) - except VcsError: - ref = remote = None - if not remote: - return None - - match = re.match('refs/remotes/([^/]+)/', remote) - if match: - return self._git(['config', '--get', 'remote.{0:s}.url'.format(match.group(1))], - catchout=True).strip() \ - or None - return None - - - def get_revision_id(self, rev=None): - """Get a canonical key for the revision rev""" - if rev is None: - rev = self.HEAD - elif rev == self.INDEX: - return None - rev = self._sanitize_rev(rev) - - return self._sanitize_rev(self._git(['rev-parse', rev], catchout=True)) - def get_info(self, rev=None): """Gets info about the given revision rev""" if rev is None: rev = self.HEAD - rev = self._sanitize_rev(rev) - if rev == self.HEAD and not self._has_head(): - return None log = self._log(refspec=rev) if len(log) == 0: - raise VcsError("Revision {0:s} does not exist".format(rev)) + if rev == self.HEAD: + return None + else: + raise VcsError('Revision {0:s} does not exist'.format(rev)) elif len(log) > 1: - raise VcsError("More than one instance of revision {0:s} ?!?".format(rev)) + raise VcsError('More than one instance of revision {0:s} ?!?'.format(rev)) else: return log[0] - - def get_files(self, rev=None): - """Gets a list of files in revision rev""" - if rev is None: - rev = self.HEAD - rev = self._sanitize_rev(rev) - if rev is None: - return [] - - if rev == self.INDEX: - return self._git(['ls-files', '-z'], - catchout=True, bytes=True).decode('utf-8').split('\x00') - else: - return self._git(['ls-tree', '--name-only', '-r', '-z', rev], - catchout=True, bytes=True).decode('utf-8').split('\x00') diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 48e991bf..58651795 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -2,31 +2,24 @@ import os import re -import shutil from datetime import datetime -try: - from configparser import RawConfigParser -except ImportError: - from ConfigParser import RawConfigParser from .vcs import Vcs, VcsError - class Hg(Vcs): HEAD = 'tip' - # Auxiliar stuff + + # Generic #--------------------------- def _hg(self, path, args, silent=True, catchout=False, bytes=False): return self._vcs(path, 'hg', args, silent=silent, catchout=catchout, bytes=bytes) - def _has_head(self): """Checks whether repo has head""" rnum = self._hg(self.path, ['-q', 'identify', '--num', '-r', self.HEAD], catchout=True) return rnum != '-1' - def _sanitize_rev(self, rev): if rev == None: return None rev = rev.strip() @@ -40,7 +33,6 @@ class Hg(Vcs): return rev - def _log(self, refspec=None, maxres=None, filelist=None): fmt = "changeset: {rev}:{node}\ntag: {tags}\nuser: {author}\ndate: {date}\nsummary: {desc}\n" @@ -66,7 +58,6 @@ class Hg(Vcs): log.append(dt) return log - def _hg_file_status(self, st): if len(st) != 1: raise VcsError("Wrong hg file status string: %s" % st) if st in "ARM": return 'staged' @@ -77,74 +68,19 @@ class Hg(Vcs): elif st in "C": return 'sync' else: return 'unknown' - - - # Repo creation - #--------------------------- - - def init(self): - """Initializes a repo in current path""" - self._hg(self.path, ['init']) - self.update() - - - def clone(self, src): - """Clones a repo from src""" - name = os.path.basename(self.path) - path = os.path.dirname(self.path) - try: - os.rmdir(self.path) - except OSError: - raise VcsError("Can't clone to %s. It is not an empty directory" % self.path) - - self._hg(path, ['clone', src, name]) - self.update() - - - # Action Interface #--------------------------- - def commit(self, message): - """Commits with a given message""" - self._hg(self.path, ['commit', '-m', message]) - - def add(self, filelist=None): """Adds files to the index, preparing for commit""" if filelist != None: self._hg(self.path, ['addremove'] + filelist) else: self._hg(self.path, ['addremove']) - def reset(self, filelist=None): """Removes files from the index""" if filelist == None: filelist = self.get_status_subpaths().keys() self._hg(self.path, ['forget'] + filelist) - - def pull(self): - """Pulls a hg repo""" - self._hg(self.path, ['pull', '-u']) - - - def push(self): - """Pushes a hg repo""" - self._hg(self.path, ['push']) - - - def checkout(self, rev): - """Checks out a branch or revision""" - self._hg(self.path, ['update', rev]) - - - def extract_file(self, rev, name, dest): - """Extracts a file from a given revision and stores it in dest dir""" - if rev == self.INDEX: - shutil.copyfile(os.path.join(self.path, name), dest) - else: - self._hg(self.path, ['cat', '--rev', rev, '--output', dest, name]) - - # Data Interface #--------------------------- @@ -161,14 +97,6 @@ class Hg(Vcs): ret[os.path.normpath(p.strip())] = sta return ret - - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo""" - raw = self._hg(self.path, ['status', '-i'], catchout=True, bytes=True) - L = re.findall('^I\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) - return set(L) - - def get_status_remote(self): """Checks the status of the repo regarding sync state with remote branch""" if self.get_remote() == None: @@ -190,50 +118,11 @@ class Hg(Vcs): elif not ahead and behind: return "behind" elif not ahead and not behind: return "sync" - def get_branch(self): """Returns the current named branch, if this makes sense for the backend. None otherwise""" branch = self._hg(self.path, ['branch'], catchout=True) return branch or None - - def get_log(self, filelist=None, maxres=None): - """Get the entire log for the current HEAD""" - if not self._has_head(): return [] - return self._log(refspec=None, maxres=maxres, filelist=filelist) - - - def get_raw_log(self, filelist=None): - """Gets the raw log as a string""" - if not self._has_head(): return [] - args = ['log'] - if filelist: args = args + filelist - return self._hg(self.path, args, catchout=True) - - - def get_raw_diff(self, refspec=None, filelist=None): - """Gets the raw diff as a string""" - args = ['diff', '--git'] - if refspec: args = args + [refspec] - if filelist: args = args + filelist - return self._hg(self.path, args, catchout=True) - - - def get_remote(self, rev=None): - """Returns the url for the remote repo attached to head""" - remote = self._hg(self.path, ['showconfig', 'paths.default'], catchout=True) - return remote or None - - - def get_revision_id(self, rev=None): - """Get a canonical key for the revision rev""" - if rev == None: rev = self.HEAD - elif rev == self.INDEX: return None - rev = self._sanitize_rev(rev) - - return self._sanitize_rev(self._hg(self.path, ['-q', 'identify', '--id', '-r', rev], catchout=True)) - - def get_info(self, rev=None): """Gets info about the given revision rev""" if rev == None: rev = self.HEAD @@ -247,16 +136,3 @@ class Hg(Vcs): raise VcsError("More than one instance of revision %s ?!?" % rev) else: return L[0] - - - def get_files(self, rev=None): - """Gets a list of files in revision rev""" - if rev == None: rev = self.HEAD - rev = self._sanitize_rev(rev) - - if rev: - if rev == self.INDEX: raw = self._hg(self.path, ['locate', "*"], catchout=True) - else: raw = self._hg(self.path, ['locate', '--rev', rev, "*"], catchout=True) - return raw.split('\n') - else: - return [] diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index f7e4bbf4..94617b1c 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -3,28 +3,23 @@ from __future__ import with_statement import os import re -import shutil import logging from datetime import datetime from .vcs import Vcs, VcsError -#logging.basicConfig(filename='rangersvn.log',level=logging.DEBUG, -# filemode='w') - class SVN(Vcs): HEAD = 'HEAD' - # Auxiliar stuff + + # Generic #--------------------------- def _svn(self, path, args, silent=True, catchout=False, bytes=False): return self._vcs(path, 'svn', args, silent=silent, catchout=catchout, bytes=bytes) - def _has_head(self): """Checks whether repo has head""" return True - def _sanitize_rev(self, rev): if rev == None: return None rev = rev.strip() @@ -38,7 +33,6 @@ class SVN(Vcs): return rev - def _log(self, refspec=None, maxres=None, filelist=None): """ Retrieves log message and parses it. """ @@ -77,7 +71,6 @@ class SVN(Vcs): logging.debug(log) return log - def _svn_file_status(self, st): if len(st) != 1: raise VcsError("Wrong hg file status string: %s" % st) if st in "A": return 'staged' @@ -87,77 +80,19 @@ class SVN(Vcs): elif st in "C": return 'conflict' else: return 'unknown' - - - # Repo creation - #--------------------------- - - def init(self): - """Initializes a repo in current path""" - self._svn(self.path, ['init']) - self.update() - - - def clone(self, src): - """Checks out SVN repo""" - name = os.path.basename(self.path) - path = os.path.dirname(self.path) - try: - os.rmdir(self.path) - except OSError: - raise VcsError("Can't clone to %s. It is not an empty directory" % self.path) - - self._svn(path, ['co', src, name]) - self.update() - - - # Action Interface #--------------------------- - def commit(self, message): - """Commits with a given message""" - self._svn(self.path, ['commit', '-m', message]) - - def add(self, filelist=None): """Adds files to the index, preparing for commit""" if filelist != None: self._svn(self.path, ['add'] + filelist) else: self._svn(self.path, ['add']) - def reset(self, filelist=None): """Equivalent to svn revert""" if filelist == None: filelist = self.get_status_subpaths().keys() self._svn(self.path, ['revert'] + filelist) - - def pull(self): - """Executes SVN Update""" - self._svn(self.path, ['update']) - - - def push(self): - """Push doesn't have an SVN analog.""" - raise NotImplementedError - - - def checkout(self, rev): - """Checks out a branch or revision""" - raise NotImplementedError - self._svn(self.path, ['update', rev]) - - - def extract_file(self, rev, name, dest): - """Extracts a file from a given revision and stores it in dest dir""" - if rev == self.INDEX: - shutil.copyfile(os.path.join(self.path, name), dest) - else: - file_contents = self._svn(self.path, ['cat', '-r', rev, name], catchout=True) - with open(dest, 'w') as f: - f.write(file_contents) - - # Data Interface #--------------------------- @@ -175,15 +110,6 @@ class SVN(Vcs): ret[os.path.normpath(p.strip())] = sta return ret - - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo""" - raw = self._svn(self.path, ['status'], catchout=True, bytes=True) -# logging.debug(raw) - L = re.findall(r'^I\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) - return set(L) - - def get_status_remote(self): """Checks the status of the repo regarding sync state with remote branch. @@ -196,47 +122,6 @@ class SVN(Vcs): branch = self._svn(self.path, ['branch'], catchout=True) return branch or None - - def get_log(self, filelist=None, maxres=None): - """Get the entire log for the current HEAD""" - if not self._has_head(): return [] - return self._log(refspec=None, maxres=maxres, filelist=filelist) - - - def get_raw_log(self, filelist=None): - """Gets the raw log as a string""" - if not self._has_head(): return [] - args = ['log'] - if filelist: args = args + filelist - return self._svn(self.path, args, catchout=True) - - - def get_raw_diff(self, refspec=None, filelist=None): - """Gets the raw diff as a string""" - args = ['diff', '--git'] - if refspec: args = args + [refspec] - if filelist: args = args + filelist - return self._svn(self.path, args, catchout=True) - - - def get_remote(self, rev=None): - """Returns the url for the remote repo attached to head""" - raw = self.get_info(rev=rev) - L = re.findall('URL: (.*)\n', raw.decode('utf-8')) - - return L[0][0] - - - def get_revision_id(self, rev=None): - """Get a canonical key for the revision rev. - - This is just returning the rev for SVN""" - if rev == None: rev = self.HEAD - elif rev == self.INDEX: return None - rev = self._sanitize_rev(rev) - return rev - - def get_info(self, rev=None): """Gets info about the given revision rev""" if rev == None: rev = self.HEAD @@ -251,12 +136,3 @@ class SVN(Vcs): raise VcsError("More than one instance of revision %s ?!?" % rev) else: return L[0] - - - def get_files(self, rev=None): - """Gets a list of files in revision rev""" - if rev == None: rev = self.HEAD - rev = self._sanitize_rev(rev) - - raw = self._svn(self.path, ['ls', "-r", rev], catchout=True) - return raw.split('\n') diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 241038e0..69bfcf74 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -66,7 +66,7 @@ class Vcs(object): self.in_repodir = False self.track = False - self.root, self.repodir, self.repotype, self.links = self.find_root(self.path) + self.root, self.repodir, self.repotype, self.links = self._find_root(self.path) self.is_root = True if self.obj.path == self.root else False if self.root: @@ -102,11 +102,11 @@ class Vcs(object): self.track = self.rootvcs.track - # Auxiliar + # Generic #--------------------------- def _vcs(self, path, cmd, args, silent=False, catchout=False, bytes=False): - """Executes a vcs command""" + """Executes a VCS command""" with open(os.devnull, 'w') as devnull: out = devnull if silent else None try: @@ -121,10 +121,7 @@ class Vcs(object): except FileNotFoundError: raise VcsError("{0:s} error on {1:s}: File not found".format(cmd, path)) - # Generic - #--------------------------- - - def get_repotype(self, path): + def _get_repotype(self, path): """Returns the right repo type for path. None if no repo present in path""" for repotype in self.repotypes_settings: repodir = os.path.join(path, '.{0:s}'.format(repotype)) @@ -132,7 +129,7 @@ class Vcs(object): return (repodir, repotype) return (None, None) - def find_root(self, path): + def _find_root(self, path): """Finds the repository root path. Otherwise returns none""" links = set() while True: @@ -141,7 +138,7 @@ class Vcs(object): relpath = os.path.relpath(self.path, path) path = os.path.realpath(path) self.path = os.path.normpath(os.path.join(path, relpath)) - repodir, repotype = self.get_repotype(path) + repodir, repotype = self._get_repotype(path) if repodir: return (path, repodir, repotype, links) if path == '/': @@ -149,29 +146,6 @@ class Vcs(object): path = os.path.dirname(path) return (None, None, None, None) - def check(self): - """Check repository health""" - if not self.in_repodir \ - and (not self.track or (not self.is_root and self.get_repotype(self.path)[0])): - self.__init__(self.obj) - elif self.track and not os.path.exists(self.repodir): - self.update_tree(purge=True) - return False - return True - - def update_root(self): - """Update repository""" - try: - self.rootvcs.head = self.rootvcs.get_info(self.HEAD) - self.rootvcs.branch = self.rootvcs.get_branch() - self.rootvcs.status_subpaths = self.rootvcs.get_status_subpaths() - self.rootvcs.remotestatus = self.rootvcs.get_status_remote() - self.rootvcs.obj.vcspathstatus = self.rootvcs.get_status_root() - except VcsError: - self.update_tree(purge=True) - return False - return True - def _update_walk(self, path, purge): """Update walk""" for wroot, wdirs, _ in os.walk(path): @@ -231,43 +205,28 @@ class Vcs(object): if purge: self.rootvcs.__init__(self.rootvcs.obj) - # Action interface - #--------------------------- - - def commit(self, message): - """Commits with a given message""" - raise NotImplementedError - - def add(self, filelist): - """Adds files to the index, preparing for commit""" - raise NotImplementedError - - def reset(self, filelist): - """Removes files from the index""" - raise NotImplementedError - - def pull(self, **kwargs): - """Pulls from remote""" - raise NotImplementedError - - def push(self, **kwargs): - """Pushes to remote""" - raise NotImplementedError - - def checkout(self, rev): - """Checks out a branch or revision""" - raise NotImplementedError - - def extract_file(self, rev, name, dest): - """Extracts a file from a given revision and stores it in dest dir""" - raise NotImplementedError - - # Data - #--------------------------- + def update_root(self): + """Update repository""" + try: + self.rootvcs.head = self.rootvcs.get_info(self.HEAD) + self.rootvcs.branch = self.rootvcs.get_branch() + self.rootvcs.status_subpaths = self.rootvcs.get_status_subpaths() + self.rootvcs.remotestatus = self.rootvcs.get_status_remote() + self.rootvcs.obj.vcspathstatus = self.rootvcs.get_status_root() + except VcsError: + self.update_tree(purge=True) + return False + return True - def get_status_root_cheap(self): - """Returns the status of self.root, very cheap""" - raise NotImplementedError + def check(self): + """Check repository health""" + if not self.in_repodir \ + and (not self.track or (not self.is_root and self._get_repotype(self.path)[0])): + self.__init__(self.obj) + elif self.track and not os.path.exists(self.repodir): + self.update_tree(purge=True) + return False + return True def get_status_root(self): """Returns the status of root""" @@ -307,47 +266,41 @@ class Vcs(object): return status return 'sync' - def get_status_subpaths(self): - """Returns a dict indexed by subpaths not in sync their status as values. - Paths are given relative to self.root. Strips trailing '/' from dirs.""" - raise NotImplementedError + # Action interface + #--------------------------- - def get_status_remote(self): - """Checks the status of the entire repo""" + def add(self, filelist): + """Adds files to the index, preparing for commit""" raise NotImplementedError - def get_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" + def reset(self, filelist): + """Removes files from the index""" raise NotImplementedError - def get_log(self): - """Get the entire log for the current HEAD""" - raise NotImplementedError + # Data interface + #--------------------------- - def get_raw_log(self, filelist=None): - """Gets the raw log as a string""" + def get_status_root_cheap(self): + """Returns the status of self.root cheaply""" raise NotImplementedError - def get_raw_diff(self, refspec=None, filelist=None): - """Gets the raw diff as a string""" + def get_status_subpaths(self): + """Returns a dict indexed by subpaths not in sync with their status as values. + Paths are given relative to self.root. Strips trailing '/' from dirs.""" raise NotImplementedError - def get_remote(self): - """Returns the url for the remote repo attached to head""" + def get_status_remote(self): + """Checks the status of the entire repo""" raise NotImplementedError - def get_revision_id(self, rev=None): - """Get a canonical key for the revision rev""" + def get_branch(self): + """Returns the current named branch, if this makes sense for the backend. None otherwise""" raise NotImplementedError def get_info(self, rev=None): """Gets info about the given revision rev""" raise NotImplementedError - def get_files(self, rev=None): - """Gets a list of files in revision rev""" - raise NotImplementedError - class VcsThread(threading.Thread): """Vcs thread""" def __init__(self, ui, idle_delay): @@ -379,7 +332,7 @@ class VcsThread(threading.Thread): # Redraw if tree is purged if not target.vcs.check(): redraw = True - if target.vcs.track and not target.vcs.root in roots: + if target.vcs.track and target.vcs.root not in roots: roots.add(target.vcs.root) # Do not update repo when repodir is displayed (causes strobing) if tuple(clmn for clmn in self.ui.browser.columns -- cgit 1.4.1-2-gfad0 From a9000364629d569340418f78bccbfd3905ff57e3 Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 18 Dec 2015 19:45:19 +0100 Subject: VCS: Update comments, Minor fixes --- ranger/config/commands.py | 4 +- ranger/container/directory.py | 9 ++- ranger/container/fsobject.py | 2 +- ranger/ext/vcs/__init__.py | 3 + ranger/ext/vcs/bzr.py | 31 ++++------ ranger/ext/vcs/git.py | 63 ++++++++----------- ranger/ext/vcs/hg.py | 32 +++++----- ranger/ext/vcs/svn.py | 24 +++++--- ranger/ext/vcs/vcs.py | 120 +++++++++++++++++++----------------- ranger/gui/widgets/__init__.py | 2 +- ranger/gui/widgets/browsercolumn.py | 6 +- ranger/gui/widgets/statusbar.py | 4 +- 12 files changed, 147 insertions(+), 153 deletions(-) diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 6012ed4e..4197ca7b 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1361,7 +1361,7 @@ class stage(Command): if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: filelist = [f.path for f in self.fm.thistab.get_selection()] try: - self.fm.thisdir.vcs.add(filelist) + self.fm.thisdir.vcs.action_add(filelist) except VcsError as error: self.fm.notify('Unable to stage files: {0:s}'.format(str(error))) self.fm.ui.vcsthread.wakeup() @@ -1380,7 +1380,7 @@ class unstage(Command): if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track: filelist = [f.path for f in self.fm.thistab.get_selection()] try: - self.fm.thisdir.vcs.reset(filelist) + self.fm.thisdir.vcs.action_reset(filelist) except VcsError as error: self.fm.notify('Unable to unstage files: {0:s}'.format(str(error))) self.fm.ui.vcsthread.wakeup() diff --git a/ranger/container/directory.py b/ranger/container/directory.py index a31d4ec9..608eda06 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -307,8 +307,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): disk_usage = 0 if self.vcs and self.vcs.track and not self.vcs.is_root: - self.vcspathstatus = self.vcs.get_status_subpath( - self.path, is_directory=True) + self.vcsstatus = self.vcs.status_subpath(self.path, is_directory=True) for name in filenames: try: @@ -341,9 +340,9 @@ class Directory(FileSystemObject, Accumulator, Loadable): if item.vcs.is_root: self.has_vcschild = True elif item.is_link and os.path.realpath(item.path) == item.vcs.root: - item.vcspathstatus = item.vcs.rootvcs.get_status_root() + item.vcsstatus = item.vcs.rootvcs.status_root() else: - item.vcspathstatus = item.vcs.get_status_subpath( + item.vcsstatus = item.vcs.status_subpath( item.path, is_directory=True) else: item = File(name, preload=stats, path_is_abs=True, @@ -351,7 +350,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): item.load() disk_usage += item.size if self.vcs and self.vcs.track: - item.vcspathstatus = self.vcs.get_status_subpath(item.path) + item.vcsstatus = self.vcs.status_subpath(item.path) files.append(item) self.percent = 100 * len(files) // len(filenames) diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index c2a8004c..caf6974a 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -73,7 +73,7 @@ class FileSystemObject(FileManagerAware, SettingsAware): size = 0 - vcspathstatus = None + vcsstatus = None _linemode = DEFAULT_LINEMODE linemode_dict = dict( diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index 3e62310c..36bbb369 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -1,3 +1,6 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + """VCS Extension""" from .vcs import Vcs, VcsError, VcsThread diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 358db8ad..a771dbb4 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -1,20 +1,22 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + """GNU Bazaar module""" import os import re from datetime import datetime - from .vcs import Vcs, VcsError class Bzr(Vcs): - """VCS implementiation for Bzr""" + """VCS implementation for GNU Bazaar""" HEAD="last:1" # Generic #--------------------------- - def _bzr(self, path, args, silent=True, catchout=False, bytes=False): - return self._vcs(path, 'bzr', args, silent=silent, catchout=catchout, bytes=bytes) + 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""" @@ -67,23 +69,19 @@ class Bzr(Vcs): # Action Interface #--------------------------- - def add(self, filelist=None): - """Adds files to the index, preparing for commit""" + def action_add(self, filelist=None): if filelist != None: self._bzr(self.path, ['add'] + filelist) else: self._bzr(self.path, ['add']) - def reset(self, filelist=None): - """Removes files from the index""" + 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']) # Data Interface #--------------------------- - def get_status_subpaths(self): - """Returns a dict indexed by files not in sync their status as values. - Paths are given relative to the root. Strips trailing '/' from dirs.""" - raw = self._bzr(self.path, ['status', '--short', '--no-classify'], catchout=True, bytes=True) + 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: @@ -91,8 +89,7 @@ class Bzr(Vcs): ret[os.path.normpath(p.strip())] = sta return ret - def get_status_remote(self): - """Returns the url for the remote repo attached to head""" + def data_status_remote(self): try: remote = self._bzr(self.path, ['config', 'parent_location'], catchout=True) except VcsError: @@ -100,13 +97,11 @@ class Bzr(Vcs): return remote.strip() or None - def get_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" + def data_branch(self): branch = self._bzr(self.path, ['nick'], catchout=True) return branch or None - def get_info(self, rev=None): - """Gets info about the given revision rev""" + 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 [] diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index ab7f516f..0de8f770 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -1,10 +1,12 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + """Git module""" import os import re from datetime import datetime import json - from .vcs import Vcs, VcsError class Git(Vcs): @@ -25,17 +27,17 @@ class Git(Vcs): # Generic #--------------------------- - def _git(self, args, path=None, silent=True, catchout=False, bytes=False): - """Call git""" + def _git(self, args, path=None, silent=True, catchout=False, retbytes=False): + """Run a git command""" return self._vcs(path or self.path, 'git', args, silent=silent, - catchout=catchout, bytes=bytes) + catchout=catchout, retbytes=retbytes) def _head_ref(self): - """Gets HEAD's ref""" + """Returns HEAD reference""" return self._git(['symbolic-ref', self.HEAD], catchout=True, silent=True) or None def _remote_ref(self, ref): - """Gets remote ref associated to given ref""" + """Returns remote reference associated to given ref""" if ref is None: return None return self._git(['for-each-ref', '--format=%(upstream)', ref], @@ -43,13 +45,15 @@ class Git(Vcs): or None def _log(self, refspec=None, maxres=None, filelist=None): - """Gets a list of dicts containing revision info, for the revisions matching refspec""" + """Returns an array of dicts containing revision info for refspec""" args = [ '--no-pager', 'log', '--pretty={' '%x00short%x00:%x00%h%x00,' - '%x00revid%x00: %x00%H%x00,' - '%x00author%x00: %x00%an <%ae>%x00, %x00date%x00: %ct, %x00summary%x00: %x00%s%x00' + '%x00revid%x00:%x00%H%x00,' + '%x00author%x00:%x00%an <%ae>%x00,' + '%x00date%x00:%ct,' + '%x00summary%x00:%x00%s%x00' '}' ] if refspec: @@ -82,30 +86,21 @@ class Git(Vcs): # Action interface #--------------------------- - def add(self, filelist=None): - """Adds files to the index, preparing for commit""" - if filelist: - self._git(['add', '--all'] + filelist) - else: - self._git(['add', '--all']) + def action_add(self, filelist=[]): + self._git(['add', '--all'] + filelist) - def reset(self, filelist=None): - """Removes files from the index""" - if filelist: - self._git(['reset'] + filelist) - else: - self._git(['reset']) + def action_reset(self, filelist=[]): + self._git(['reset'] + filelist) # Data Interface #--------------------------- - def get_status_root_cheap(self): - """Returns the status of root, very cheap""" + def data_status_root(self): statuses = set() # Paths with status skip = False for line in self._git(['status', '--porcelain', '-z'], - catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1]: + catchout=True, retbytes=True).decode('utf-8').split('\x00')[:-1]: if skip: skip = False continue @@ -113,19 +108,18 @@ class Git(Vcs): if line.startswith('R'): skip = True - for status in self.DIR_STATUS: + for status in self.DIRSTATUSES: if status in statuses: return status return 'sync' - def get_status_subpaths(self): - """Returns a dict (path: status) for paths not in sync. Strips trailing '/' from dirs""" + def data_status_subpaths(self): statuses = {} # Ignored directories for line in self._git( ['ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'], - catchout=True, bytes=True + catchout=True, retbytes=True ).decode('utf-8').split('\x00')[:-1]: if line.endswith('/'): statuses[os.path.normpath(line)] = 'ignored' @@ -133,7 +127,7 @@ class Git(Vcs): # Empty directories for line in self._git( ['ls-files', '-z', '--others', '--directory', '--exclude-standard'], - catchout=True, bytes=True + catchout=True, retbytes=True ).decode('utf-8').split('\x00')[:-1]: if line.endswith('/'): statuses[os.path.normpath(line)] = 'none' @@ -141,7 +135,7 @@ class Git(Vcs): # Paths with status skip = False for line in self._git(['status', '--porcelain', '-z', '--ignored'], - catchout=True, bytes=True).decode('utf-8').split('\x00')[:-1]: + catchout=True, retbytes=True).decode('utf-8').split('\x00')[:-1]: if skip: skip = False continue @@ -151,8 +145,7 @@ class Git(Vcs): return statuses - def get_status_remote(self): - """Checks the status of the repo regarding sync state with remote branch""" + def data_status_remote(self): try: head = self._head_ref() remote = self._remote_ref(head) @@ -170,8 +163,7 @@ class Git(Vcs): else: return 'behind' if behind else 'sync' - def get_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" + def data_branch(self): try: head = self._head_ref() except VcsError: @@ -185,8 +177,7 @@ class Git(Vcs): else: return None - def get_info(self, rev=None): - """Gets info about the given revision rev""" + def data_info(self, rev=None): if rev is None: rev = self.HEAD diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 58651795..39a81d57 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -1,19 +1,22 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + """Mercurial module""" import os import re from datetime import datetime - from .vcs import Vcs, VcsError class Hg(Vcs): + """VCS implementation for Mercurial""" HEAD = 'tip' # Generic #--------------------------- - def _hg(self, path, args, silent=True, catchout=False, bytes=False): - return self._vcs(path, 'hg', args, silent=silent, catchout=catchout, bytes=bytes) + def _hg(self, path, args, silent=True, catchout=False, retbytes=False): + return self._vcs(path, 'hg', args, silent=silent, catchout=catchout, retbytes=retbytes) def _has_head(self): """Checks whether repo has head""" @@ -71,23 +74,19 @@ class Hg(Vcs): # Action Interface #--------------------------- - def add(self, filelist=None): - """Adds files to the index, preparing for commit""" + def action_add(self, filelist=None): if filelist != None: self._hg(self.path, ['addremove'] + filelist) else: self._hg(self.path, ['addremove']) - def reset(self, filelist=None): - """Removes files from the index""" - if filelist == None: filelist = self.get_status_subpaths().keys() + def action_reset(self, filelist=None): + if filelist == None: filelist = self.data_status_subpaths().keys() self._hg(self.path, ['forget'] + filelist) # Data Interface #--------------------------- - def get_status_subpaths(self): - """Returns a dict indexed by files not in sync their status as values. - Paths are given relative to the root. Strips trailing '/' from dirs.""" - raw = self._hg(self.path, ['status'], catchout=True, bytes=True) + def data_status_subpaths(self): + raw = self._hg(self.path, ['status'], catchout=True, retbytes=True) L = re.findall('^(.)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) ret = {} for st, p in L: @@ -97,8 +96,7 @@ class Hg(Vcs): ret[os.path.normpath(p.strip())] = sta return ret - def get_status_remote(self): - """Checks the status of the repo regarding sync state with remote branch""" + def data_status_remote(self): if self.get_remote() == None: return "none" @@ -118,13 +116,11 @@ class Hg(Vcs): elif not ahead and behind: return "behind" elif not ahead and not behind: return "sync" - def get_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" + def data_branch(self): branch = self._hg(self.path, ['branch'], catchout=True) return branch or None - def get_info(self, rev=None): - """Gets info about the given revision rev""" + 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 None diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 94617b1c..a8ee66f3 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -1,3 +1,6 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + """Subversion module""" from __future__ import with_statement @@ -8,13 +11,14 @@ from datetime import datetime from .vcs import Vcs, VcsError class SVN(Vcs): + """VCS implementation for Subversion""" HEAD = 'HEAD' # Generic #--------------------------- - def _svn(self, path, args, silent=True, catchout=False, bytes=False): - return self._vcs(path, 'svn', args, silent=silent, catchout=catchout, bytes=bytes) + def _svn(self, path, args, silent=True, catchout=False, retbytes=False): + return self._vcs(path, 'svn', args, silent=silent, catchout=catchout, retbytes=retbytes) def _has_head(self): """Checks whether repo has head""" @@ -83,23 +87,23 @@ class SVN(Vcs): # Action Interface #--------------------------- - def add(self, filelist=None): + def action_add(self, filelist=None): """Adds files to the index, preparing for commit""" if filelist != None: self._svn(self.path, ['add'] + filelist) else: self._svn(self.path, ['add']) - def reset(self, filelist=None): + def action_reset(self, filelist=None): """Equivalent to svn revert""" - if filelist == None: filelist = self.get_status_subpaths().keys() + if filelist == None: filelist = self.data_status_subpaths().keys() self._svn(self.path, ['revert'] + filelist) # Data Interface #--------------------------- - def get_status_subpaths(self): + def data_status_subpaths(self): """Returns a dict indexed by files not in sync their status as values. Paths are given relative to the root. Strips trailing '/' from dirs.""" - raw = self._svn(self.path, ['status'], catchout=True, bytes=True) + raw = self._svn(self.path, ['status'], catchout=True, retbytes=True) # logging.debug(raw) L = re.findall(r'^(.)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) ret = {} @@ -110,19 +114,19 @@ class SVN(Vcs): ret[os.path.normpath(p.strip())] = sta return ret - def get_status_remote(self): + def data_status_remote(self): """Checks the status of the repo regarding sync state with remote branch. I'm not sure this make sense for SVN so we're just going to return 'sync'""" return 'sync' - def get_branch(self): + def data_branch(self): """Returns the current named branch, if this makes sense for the backend. None otherwise""" return None branch = self._svn(self.path, ['branch'], catchout=True) return branch or None - def get_info(self, rev=None): + def data_info(self, rev=None): """Gets info about the given revision rev""" if rev == None: rev = self.HEAD rev = self._sanitize_rev(rev) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 69bfcf74..5018fbb9 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -1,3 +1,6 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + """VCS module""" import os @@ -5,7 +8,7 @@ import subprocess import threading class VcsError(Exception): - """Vcs exception""" + """VCS exception""" pass class Vcs(object): @@ -13,21 +16,16 @@ class Vcs(object): This class represents a version controlled path, abstracting the usual operations from the different supported backends. - The backends are declared in te variable self.repo_types, and are derived + The backends are declared in REPOTYPES, and are derived classes from Vcs with the following restrictions: - * do NOT implement __init__. Vcs takes care of this. - - * do not create change internal state. All internal state should be - handled in Vcs + * Override ALL interface methods + * Only override interface methods + * Do NOT modify internal state. All internal state is handled by Vcs - Objects from backend classes should never be created directly. Instead - create objects of Vcs class. The initialization calls update, which takes - care of detecting the right Vcs backend to use and dynamically changes the - object type accordingly. """ - # These are abstracted revs, representing the current index (staged files), + # These are abstracted revisions, representing the current index (staged files), # the current head and nothing. Every backend should redefine them if the # version control has a similar concept, or implement _sanitize_rev method to # clean the rev before using them @@ -35,6 +33,7 @@ class Vcs(object): HEAD = 'HEAD' NONE = 'NONE' + # Backends REPOTYPES = { 'git': {'class': 'Git', 'setting': 'vcs_backend_git'}, 'hg': {'class': 'Hg', 'setting': 'vcs_backend_hg'}, @@ -44,7 +43,7 @@ class Vcs(object): # Possible directory statuses in order of importance with statuses that # don't make sense disabled - DIR_STATUS = ( + DIRSTATUSES = ( 'conflict', 'untracked', 'deleted', @@ -77,15 +76,15 @@ class Vcs(object): self.status_subpaths = {} if not os.access(self.repodir, os.R_OK): - directoryobject.vcspathstatus = 'unknown' + directoryobject.vcsstatus = 'unknown' self.remotestatus = 'unknown' return try: - self.head = self.get_info(self.HEAD) - self.branch = self.get_branch() - self.remotestatus = self.get_status_remote() - self.obj.vcspathstatus = self.get_status_root_cheap() + self.head = self.data_info(self.HEAD) + self.branch = self.data_branch() + self.remotestatus = self.data_status_remote() + self.obj.vcsstatus = self.data_status_root() except VcsError: return @@ -105,14 +104,14 @@ class Vcs(object): # Generic #--------------------------- - def _vcs(self, path, cmd, args, silent=False, catchout=False, bytes=False): - """Executes a VCS command""" + def _vcs(self, path, cmd, args, silent=False, catchout=False, retbytes=False): + """Run a VCS command""" with open(os.devnull, 'w') as devnull: out = devnull if silent else None try: if catchout: output = subprocess.check_output([cmd] + args, stderr=out, cwd=path) - return output if bytes else output.decode('utf-8').strip() + return output if retbytes else output.decode('UTF-8').strip() else: subprocess.check_call([cmd] + args, stderr=out, stdout=out, cwd=path) except subprocess.CalledProcessError: @@ -122,7 +121,7 @@ class Vcs(object): raise VcsError("{0:s} error on {1:s}: File not found".format(cmd, path)) def _get_repotype(self, path): - """Returns the right repo type for path. None if no repo present in path""" + """Get type for path""" for repotype in self.repotypes_settings: repodir = os.path.join(path, '.{0:s}'.format(repotype)) if os.path.exists(repodir): @@ -130,7 +129,7 @@ class Vcs(object): return (None, None) def _find_root(self, path): - """Finds the repository root path. Otherwise returns none""" + """Finds root path""" links = set() while True: if os.path.islink(path): @@ -138,12 +137,16 @@ class Vcs(object): relpath = os.path.relpath(self.path, path) path = os.path.realpath(path) self.path = os.path.normpath(os.path.join(path, relpath)) + repodir, repotype = self._get_repotype(path) if repodir: return (path, repodir, repotype, links) - if path == '/': - break + + path_old = path path = os.path.dirname(path) + if path == path_old: + break + return (None, None, None, None) def _update_walk(self, path, purge): @@ -159,10 +162,10 @@ class Vcs(object): for fileobj in wroot_obj.files_all: if purge: if fileobj.is_directory: - fileobj.vcspathstatus = None + fileobj.vcsstatus = None fileobj.vcs.__init__(fileobj) else: - fileobj.vcspathstatus = None + fileobj.vcsstatus = None continue if fileobj.is_directory: @@ -170,10 +173,10 @@ class Vcs(object): if not fileobj.vcs.track: continue if not fileobj.vcs.is_root: - fileobj.vcspathstatus = wroot_obj.vcs.get_status_subpath( + fileobj.vcsstatus = wroot_obj.vcs.status_subpath( fileobj.path, is_directory=True) else: - fileobj.vcspathstatus = wroot_obj.vcs.get_status_subpath(fileobj.path) + fileobj.vcsstatus = wroot_obj.vcs.status_subpath(fileobj.path) # Remove dead directories for wdir in list(wdirs): @@ -186,7 +189,7 @@ class Vcs(object): wdirs.remove(wdir) def update_tree(self, purge=False): - """Update tree""" + """Update tree state""" self._update_walk(self.root, purge) for path in list(self.rootvcs.links): self._update_walk(path, purge) @@ -195,31 +198,30 @@ class Vcs(object): except KeyError: continue if purge: - dirobj.vcspathstatus = None + dirobj.vcsstatus = None dirobj.vcs.__init__(dirobj.vcs.obj) elif dirobj.vcs.path == self.root: - dirobj.vcspathstatus = self.rootvcs.get_status_root() + dirobj.vcsstatus = self.rootvcs.status_root() else: - dirobj.vcspathstatus = dirobj.vcs.get_status_subpath( - dirobj.path, is_directory=True) + dirobj.vcsstatus = dirobj.vcs.status_subpath(dirobj.path, is_directory=True) if purge: self.rootvcs.__init__(self.rootvcs.obj) def update_root(self): - """Update repository""" + """Update root state""" try: - self.rootvcs.head = self.rootvcs.get_info(self.HEAD) - self.rootvcs.branch = self.rootvcs.get_branch() - self.rootvcs.status_subpaths = self.rootvcs.get_status_subpaths() - self.rootvcs.remotestatus = self.rootvcs.get_status_remote() - self.rootvcs.obj.vcspathstatus = self.rootvcs.get_status_root() + self.rootvcs.head = self.rootvcs.data_info(self.HEAD) + self.rootvcs.branch = self.rootvcs.data_branch() + self.rootvcs.status_subpaths = self.rootvcs.data_status_subpaths() + self.rootvcs.remotestatus = self.rootvcs.data_status_remote() + self.rootvcs.obj.vcsstatus = self.rootvcs.status_root() except VcsError: self.update_tree(purge=True) return False return True def check(self): - """Check repository health""" + """Check health""" if not self.in_repodir \ and (not self.track or (not self.is_root and self._get_repotype(self.path)[0])): self.__init__(self.obj) @@ -228,15 +230,15 @@ class Vcs(object): return False return True - def get_status_root(self): - """Returns the status of root""" + def status_root(self): + """Returns root status""" statuses = set(status for path, status in self.status_subpaths.items()) - for status in self.DIR_STATUS: + for status in self.DIRSTATUSES: if status in statuses: return status return 'sync' - def get_status_subpath(self, path, is_directory=False): + def status_subpath(self, path, is_directory=False): """ Returns the status of path @@ -261,7 +263,7 @@ class Vcs(object): if is_directory: statuses = set(status for subpath, status in self.rootvcs.status_subpaths.items() if subpath.startswith(relpath + '/')) - for status in self.DIR_STATUS: + for status in self.DIRSTATUSES: if status in statuses: return status return 'sync' @@ -269,36 +271,39 @@ class Vcs(object): # Action interface #--------------------------- - def add(self, filelist): - """Adds files to the index, preparing for commit""" + def action_add(self, filelist): + """Adds files to the index""" raise NotImplementedError - def reset(self, filelist): + def action_reset(self, filelist): """Removes files from the index""" raise NotImplementedError # Data interface #--------------------------- - def get_status_root_cheap(self): - """Returns the status of self.root cheaply""" + def data_status_root(self): + """Returns status of self.root cheaply""" raise NotImplementedError - def get_status_subpaths(self): + def data_status_subpaths(self): """Returns a dict indexed by subpaths not in sync with their status as values. - Paths are given relative to self.root. Strips trailing '/' from dirs.""" + Paths are given relative to self.root""" raise NotImplementedError - def get_status_remote(self): - """Checks the status of the entire repo""" + def data_status_remote(self): + """ + Returns remote status of repository + One of ('sync', 'ahead', 'behind', 'diverged', 'none') + """ raise NotImplementedError - def get_branch(self): + def data_branch(self): """Returns the current named branch, if this makes sense for the backend. None otherwise""" raise NotImplementedError - def get_info(self, rev=None): - """Gets info about the given revision rev""" + def data_info(self, rev=None): + """Returns info string about revision rev. None in special cases""" raise NotImplementedError class VcsThread(threading.Thread): @@ -361,6 +366,7 @@ class VcsThread(threading.Thread): """Wakeup thread""" self.wake.set() +# Backend imports import ranger.ext.vcs.git import ranger.ext.vcs.hg import ranger.ext.vcs.bzr diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index 200ae8a8..ac1188ca 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -5,7 +5,7 @@ from ranger.gui.displayable import Displayable class Widget(Displayable): """A class for classification of widgets.""" - vcspathstatus_symb = {'conflict': ('X', ["vcsconflict"]), + vcsstatus_symb = {'conflict': ('X', ["vcsconflict"]), 'untracked': ('+', ["vcschanged"]), 'deleted': ('-', ["vcschanged"]), 'changed': ('*', ["vcschanged"]), diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 13c8f510..58d07072 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -257,7 +257,7 @@ class BrowserColumn(Pager): metakey = hash(repr(sorted(metadata.items()))) if metadata else 0 key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, - drawn.vcspathstatus, + drawn.vcsstatus, drawn.vcs.remotestatus \ if drawn.is_directory and drawn.vcs \ and drawn.vcs.is_root and drawn.vcs.track \ @@ -384,8 +384,8 @@ class BrowserColumn(Pager): vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) elif self.target.has_vcschild: vcsstring_display.insert(-1, [" ", []]) - if drawn.vcspathstatus: - vcsstr, vcscol = self.vcspathstatus_symb[drawn.vcspathstatus] + if drawn.vcsstatus: + vcsstr, vcscol = self.vcsstatus_symb[drawn.vcsstatus] vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) else: vcsstring_display.append([" ", []]) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 9783a7ad..978171e5 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -195,8 +195,8 @@ class StatusBar(Widget): if directory.vcs.rootvcs.remotestatus: vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.rootvcs.remotestatus] left.add(vcsstr.strip(), 'vcsremote', *vcscol) - if target.vcspathstatus: - vcsstr, vcscol = self.vcspathstatus_symb[target.vcspathstatus] + if target.vcsstatus: + vcsstr, vcscol = self.vcsstatus_symb[target.vcsstatus] left.add(vcsstr.strip(), 'vcsfile', *vcscol) if directory.vcs.rootvcs.head: left.add_space() -- cgit 1.4.1-2-gfad0 From fb79b2a9aadbf552a2845208917019ee6dc8bc13 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sat, 19 Dec 2015 21:32:49 +0100 Subject: VCS: Refactor _vcs --- ranger/ext/vcs/git.py | 76 +++++++++++++++++++++++++-------------------------- ranger/ext/vcs/vcs.py | 28 +++++++++---------- 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 0de8f770..41961795 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -27,22 +27,19 @@ class Git(Vcs): # Generic #--------------------------- - def _git(self, args, path=None, silent=True, catchout=False, retbytes=False): + def _git(self, args, path=None, catchout=True, retbytes=False): """Run a git command""" - return self._vcs(path or self.path, 'git', args, silent=silent, - catchout=catchout, retbytes=retbytes) + return self._vcs(path or self.path, 'git', args, catchout=catchout, retbytes=retbytes) def _head_ref(self): """Returns HEAD reference""" - return self._git(['symbolic-ref', self.HEAD], catchout=True, silent=True) or None + return self._git(['symbolic-ref', self.HEAD]).rstrip() or None def _remote_ref(self, ref): """Returns remote reference associated to given ref""" if ref is None: return None - return self._git(['for-each-ref', '--format=%(upstream)', ref], - catchout=True, silent=True) \ - or None + return self._git(['for-each-ref', '--format=%(upstream)', ref]).rstrip() or None def _log(self, refspec=None, maxres=None, filelist=None): """Returns an array of dicts containing revision info for refspec""" @@ -64,20 +61,20 @@ class Git(Vcs): args += ['--'] + filelist try: - log_raw = self._git(args, catchout=True)\ + output = self._git(args)\ .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines() except VcsError: - return [] + return None log = [] - for line in log_raw: + for line in output: line = json.loads(line) line['date'] = datetime.fromtimestamp(line['date']) log.append(line) return log def _git_status_translate(self, code): - """Translate git status code""" + """Translate status code""" for X, Y, status in self._status_translations: if code[0] in X and code[1] in Y: return status @@ -86,21 +83,27 @@ class Git(Vcs): # Action interface #--------------------------- - def action_add(self, filelist=[]): - self._git(['add', '--all'] + filelist) + def action_add(self, filelist=None): + args = ['add', '--all'] + if filelist: + args += ['--'] + filelist + self._git(args, catchout=False) - def action_reset(self, filelist=[]): - self._git(['reset'] + filelist) + def action_reset(self, filelist=None): + args = ['reset'] + if filelist: + args += ['--'] + filelist + self._git(args, catchout=False) # Data Interface #--------------------------- def data_status_root(self): statuses = set() + # Paths with status skip = False - for line in self._git(['status', '--porcelain', '-z'], - catchout=True, retbytes=True).decode('utf-8').split('\x00')[:-1]: + for line in self._git(['status', '--porcelain', '-z']).split('\x00')[:-1]: if skip: skip = False continue @@ -117,25 +120,21 @@ class Git(Vcs): statuses = {} # Ignored directories - for line in self._git( - ['ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'], - catchout=True, retbytes=True - ).decode('utf-8').split('\x00')[:-1]: - if line.endswith('/'): - statuses[os.path.normpath(line)] = 'ignored' + for path in self._git( + ['ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'])\ + .split('\x00')[:-1]: + if path.endswith('/'): + statuses[os.path.normpath(path)] = 'ignored' # Empty directories - for line in self._git( - ['ls-files', '-z', '--others', '--directory', '--exclude-standard'], - catchout=True, retbytes=True - ).decode('utf-8').split('\x00')[:-1]: - if line.endswith('/'): - statuses[os.path.normpath(line)] = 'none' + for path in self._git(['ls-files', '-z', '--others', '--directory', '--exclude-standard'])\ + .split('\x00')[:-1]: + if path.endswith('/'): + statuses[os.path.normpath(path)] = 'none' # Paths with status skip = False - for line in self._git(['status', '--porcelain', '-z', '--ignored'], - catchout=True, retbytes=True).decode('utf-8').split('\x00')[:-1]: + for line in self._git(['status', '--porcelain', '-z', '--ignored']).split('\x00')[:-1]: if skip: skip = False continue @@ -154,10 +153,9 @@ class Git(Vcs): if not head or not remote: return 'none' - output = self._git(['rev-list', '--left-right', '{0:s}...{1:s}'.format(remote, head)], - catchout=True) - ahead = re.search("^>", output, flags=re.MULTILINE) - behind = re.search("^<", output, flags=re.MULTILINE) + output = self._git(['rev-list', '--left-right', '{0:s}...{1:s}'.format(remote, head)]) + ahead = re.search(r'^>', output, flags=re.MULTILINE) + behind = re.search(r'^<', output, flags=re.MULTILINE) if ahead: return 'diverged' if behind else 'ahead' else: @@ -182,12 +180,12 @@ class Git(Vcs): rev = self.HEAD log = self._log(refspec=rev) - if len(log) == 0: + if not log: if rev == self.HEAD: return None else: raise VcsError('Revision {0:s} does not exist'.format(rev)) - elif len(log) > 1: - raise VcsError('More than one instance of revision {0:s} ?!?'.format(rev)) - else: + elif len(log) == 1: return log[0] + else: + 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 5018fbb9..e2d86d4b 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -104,21 +104,21 @@ class Vcs(object): # Generic #--------------------------- - def _vcs(self, path, cmd, args, silent=False, catchout=False, retbytes=False): + def _vcs(self, path, cmd, args, catchout=True, retbytes=False): """Run a VCS command""" - with open(os.devnull, 'w') as devnull: - out = devnull if silent else None - try: - if catchout: - output = subprocess.check_output([cmd] + args, stderr=out, cwd=path) - return output if retbytes else output.decode('UTF-8').strip() - else: - subprocess.check_call([cmd] + args, stderr=out, stdout=out, cwd=path) - except subprocess.CalledProcessError: - raise VcsError("{0:s} error on {1:s}. Command: {2:s}"\ - .format(cmd, path, ' '.join([cmd] + args))) - except FileNotFoundError: - raise VcsError("{0:s} error on {1:s}: File not found".format(cmd, path)) + try: + if catchout: + output = subprocess.check_output([cmd] + args, cwd=path, + stderr=subprocess.DEVNULL) + return output if retbytes else output.decode('UTF-8') + else: + subprocess.check_call([cmd] + args, cwd=path, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + raise VcsError("{0:s} error on {1:s}. Command: {2:s}"\ + .format(cmd, path, ' '.join([cmd] + args))) + except FileNotFoundError: + raise VcsError("{0:s} error on {1:s}: File not found".format(cmd, path)) def _get_repotype(self, path): """Get type for path""" -- cgit 1.4.1-2-gfad0 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 From 464986684615c8baf24b1dfb13bc150f10a38746 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 20 Dec 2015 11:59:45 +0100 Subject: VCS: Implement GNU Bazaar: Disable data_status_remote for performance --- ranger/ext/vcs/bzr.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 49ce8b34..aa7112dc 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -117,21 +117,7 @@ class Bzr(Vcs): if not self._remote_url(): return 'none' - # XXX: Find a local solution - ahead = behind = False - try: - self._bzr(['missing', '--mine-only'], catchout=False) - except VcsError: - ahead = True - try: - self._bzr(['missing', '--theirs-only'], catchout=False) - except VcsError: - behind = True - - if ahead: - return 'diverged' if behind else 'ahead' - else: - return 'behind' if behind else 'sync' + return 'unknown' def data_branch(self): try: -- cgit 1.4.1-2-gfad0 From 89a97553f9175ba6b007c594eb6f25a9310583cc Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 20 Dec 2015 12:12:55 +0100 Subject: VCS: _vcs remove args parameter --- ranger/ext/vcs/bzr.py | 2 +- ranger/ext/vcs/git.py | 2 +- ranger/ext/vcs/vcs.py | 13 +++++-------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index aa7112dc..b06f28ec 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -23,7 +23,7 @@ class Bzr(Vcs): 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 self._vcs(['bzr'] + args, path or self.path, catchout=catchout, retbytes=retbytes) def _remote_url(self): """Returns remote url""" diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 41961795..1741ceff 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -29,7 +29,7 @@ class Git(Vcs): def _git(self, args, path=None, catchout=True, retbytes=False): """Run a git command""" - return self._vcs(path or self.path, 'git', args, catchout=catchout, retbytes=retbytes) + return self._vcs(['git'] + args, path or self.path, catchout=catchout, retbytes=retbytes) def _head_ref(self): """Returns HEAD reference""" diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index ae770699..fbcfc9c4 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -106,21 +106,18 @@ class Vcs(object): # Generic #--------------------------- - def _vcs(self, path, cmd, args, catchout=True, retbytes=False): + def _vcs(self, cmd, path, catchout=True, retbytes=False): """Run a VCS command""" try: if catchout: - output = subprocess.check_output([cmd] + args, cwd=path, + output = subprocess.check_output(cmd, cwd=path, stderr=subprocess.DEVNULL) return output if retbytes else output.decode('UTF-8') else: - subprocess.check_call([cmd] + args, cwd=path, + subprocess.check_call(cmd, cwd=path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessError: - raise VcsError("{0:s} error on {1:s}. Command: {2:s}"\ - .format(cmd, path, ' '.join([cmd] + args))) - except FileNotFoundError: - raise VcsError("{0:s} error on {1:s}: File not found".format(cmd, path)) + except (subprocess.CalledProcessError, FileNotFoundError): + raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) def _get_repotype(self, path): """Get type for path""" -- cgit 1.4.1-2-gfad0 From 5374beaf5f5022a33889a7ebfa00fa9aaa07f670 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 21 Dec 2015 00:56:09 +0100 Subject: VCS: Improve repodir detection --- ranger/ext/vcs/vcs.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index fbcfc9c4..1c62afe7 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -345,6 +345,18 @@ class VcsThread(threading.Thread): return True return False + def _targeted_directory_rightmost(self): + """Return rightmost targeted directory""" + target = self.ui.browser.columns[-1].target + if target: + if target.is_directory: + return target + else: + target = self.ui.browser.columns[-2].target + if target and target.is_directory: + return target + return None + def run(self): roots = set() # already updated roots redraw = False @@ -354,6 +366,11 @@ class VcsThread(threading.Thread): self.wake.clear() continue + # Exclude root if repodir in the rightmost column (causes strobing) + target = self._targeted_directory_rightmost() + if target and target.vcs and target.vcs.in_repodir: + roots.add(target.vcs.root) + for column in self.ui.browser.columns: target = column.target if target and target.is_directory and target.vcs: @@ -362,12 +379,6 @@ class VcsThread(threading.Thread): redraw = True if target.vcs.track and target.vcs.root not in roots: roots.add(target.vcs.root) - # Do not update repo when repodir is displayed (causes strobing) - if tuple(clmn for clmn in self.ui.browser.columns - if clmn.target - and (clmn.target.path == target.vcs.repodir or - clmn.target.path.startswith(target.vcs.repodir + '/'))): - continue lazy = target.vcs.REPOTYPES[target.vcs.repotype]['lazy'] if (target.vcs.rootvcs.status_subpaths is None \ or (lazy and target.vcs.check_outdated()) \ @@ -375,6 +386,7 @@ class VcsThread(threading.Thread): and target.vcs.update_root(): target.vcs.update_tree() redraw = True + roots.clear() if redraw: redraw = False @@ -385,7 +397,6 @@ class VcsThread(threading.Thread): if self.wake.is_set(): self.ui.redraw() - roots.clear() self.wake.clear() self.wake.wait(timeout=self.delay) -- cgit 1.4.1-2-gfad0 From 3fe88d2be06920e5a477963f6c5816e8d2a6f19c Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 21 Dec 2015 00:56:14 +0100 Subject: VCS: Move remotestatus to FileSystemObject --- ranger/container/directory.py | 16 +++++----------- ranger/container/fsobject.py | 1 + ranger/ext/vcs/vcs.py | 16 ++++++++++------ ranger/gui/widgets/__init__.py | 36 ++++++++++++++++++++---------------- ranger/gui/widgets/browsercolumn.py | 13 ++++--------- ranger/gui/widgets/statusbar.py | 4 ++-- 6 files changed, 42 insertions(+), 44 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 608eda06..9e88d265 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -306,9 +306,6 @@ class Directory(FileSystemObject, Accumulator, Loadable): files = [] disk_usage = 0 - if self.vcs and self.vcs.track and not self.vcs.is_root: - self.vcsstatus = self.vcs.status_subpath(self.path, is_directory=True) - for name in filenames: try: file_lstat = os_lstat(name) @@ -335,15 +332,12 @@ class Directory(FileSystemObject, Accumulator, Loadable): else: item.relative_path = item.basename item.relative_path_lower = item.relative_path.lower() - - if item.vcs and item.vcs.track: - if item.vcs.is_root: - self.has_vcschild = True - elif item.is_link and os.path.realpath(item.path) == item.vcs.root: - item.vcsstatus = item.vcs.rootvcs.status_root() + if item.vcs and item.vcs.track and item.is_link: + if os.path.realpath(item.path) == item.vcs.root: + item.vcsstatus = item.vcs.rootvcs.obj.vcsstatus + item.vcsremotestatus = item.vcs.rootvcs.obj.vcsremotestatus else: - item.vcsstatus = item.vcs.status_subpath( - item.path, is_directory=True) + item.vcsstatus = item.vcs.status_subpath(item.path) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py index caf6974a..1daf6d70 100644 --- a/ranger/container/fsobject.py +++ b/ranger/container/fsobject.py @@ -74,6 +74,7 @@ class FileSystemObject(FileManagerAware, SettingsAware): size = 0 vcsstatus = None + vcsremotestatus = None _linemode = DEFAULT_LINEMODE linemode_dict = dict( diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 1c62afe7..2552d955 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -77,14 +77,14 @@ class Vcs(object): self.status_subpaths = None if not os.access(self.repodir, os.R_OK): - directoryobject.vcsstatus = 'unknown' - self.remotestatus = 'unknown' + self.obj.vcsremotestatus = 'unknown' + self.obj.vcsstatus = 'unknown' return try: self.head = self.data_info(self.HEAD) self.branch = self.data_branch() - self.remotestatus = self.data_status_remote() + self.obj.vcsremotestatus = self.data_status_remote() self.obj.vcsstatus = self.data_status_root() except VcsError: return @@ -158,6 +158,7 @@ class Vcs(object): wdirs[:] = [] continue if wroot_obj.content_loaded: + has_vcschild = False for fileobj in wroot_obj.files_all: if purge: if fileobj.is_directory: @@ -171,11 +172,14 @@ class Vcs(object): fileobj.vcs.check() if not fileobj.vcs.track: continue - if not fileobj.vcs.is_root: + if fileobj.vcs.is_root: + has_vcschild = True + else: fileobj.vcsstatus = wroot_obj.vcs.status_subpath( fileobj.path, is_directory=True) else: fileobj.vcsstatus = wroot_obj.vcs.status_subpath(fileobj.path) + wroot_obj.has_vcschild = has_vcschild # Remove dead directories for wdir in list(wdirs): @@ -198,7 +202,7 @@ class Vcs(object): continue if purge: dirobj.vcsstatus = None - dirobj.vcs.__init__(dirobj.vcs.obj) + dirobj.vcs.__init__(dirobj) elif dirobj.vcs.path == self.root: dirobj.vcsstatus = self.rootvcs.status_root() else: @@ -212,7 +216,7 @@ class Vcs(object): self.rootvcs.head = self.rootvcs.data_info(self.HEAD) self.rootvcs.branch = self.rootvcs.data_branch() self.rootvcs.status_subpaths = self.rootvcs.data_status_subpaths() - self.rootvcs.remotestatus = self.rootvcs.data_status_remote() + self.rootvcs.obj.vcsremotestatus = self.rootvcs.data_status_remote() self.rootvcs.obj.vcsstatus = self.rootvcs.status_root() except VcsError: self.update_tree(purge=True) diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index ac1188ca..ca3e90ce 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -5,21 +5,25 @@ from ranger.gui.displayable import Displayable class Widget(Displayable): """A class for classification of widgets.""" - vcsstatus_symb = {'conflict': ('X', ["vcsconflict"]), - 'untracked': ('+', ["vcschanged"]), - 'deleted': ('-', ["vcschanged"]), - 'changed': ('*', ["vcschanged"]), - 'staged': ('*', ["vcsstaged"]), - 'ignored': ('·', ["vcsignored"]), - 'sync': ('√', ["vcssync"]), - 'none': (' ', []), - 'unknown': ('?', ["vcsunknown"])} + vcsstatus_symb = { + 'conflict': ('X', ["vcsconflict"]), + 'untracked': ('+', ["vcschanged"]), + 'deleted': ('-', ["vcschanged"]), + 'changed': ('*', ["vcschanged"]), + 'staged': ('*', ["vcsstaged"]), + 'ignored': ('·', ["vcsignored"]), + 'sync': ('√', ["vcssync"]), + 'none': (' ', []), + 'unknown': ('?', ["vcsunknown"]), + } - vcsremotestatus_symb = {'none': ('⌂', ["vcsnone"]), - 'sync': ('=', ["vcssync"]), - 'behind': ('<', ["vcsbehind"]), - 'ahead': ('>', ["vcsahead"]), - 'diverged': ('Y', ["vcsdiverged"]), - 'unknown': ('?', ["vcsunknown"])} + vcsremotestatus_symb = { + 'diverged': ('Y', ["vcsdiverged"]), + 'ahead': ('>', ["vcsahead"]), + 'behind': ('<', ["vcsbehind"]), + 'sync': ('=', ["vcssync"]), + 'none': ('⌂', ["vcsnone"]), + 'unknown': ('?', ["vcsunknown"]), + } - ellipsis = { False: '~', True: '…' } + ellipsis = {False: '~', True: '…'} diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 58d07072..944f5c58 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -257,13 +257,8 @@ class BrowserColumn(Pager): metakey = hash(repr(sorted(metadata.items()))) if metadata else 0 key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, - drawn.vcsstatus, - drawn.vcs.remotestatus \ - if drawn.is_directory and drawn.vcs \ - and drawn.vcs.is_root and drawn.vcs.track \ - else None, - self.fm.do_cut, - current_linemode.name, metakey) + drawn.vcsstatus, drawn.vcsremotestatus, + self.fm.do_cut, current_linemode.name, metakey) if key in drawn.display_data: self.execute_curses_batch(line, drawn.display_data[key]) @@ -379,8 +374,8 @@ class BrowserColumn(Pager): vcsstring_display = [] directory = drawn if drawn.is_directory else self.target if directory.vcs and directory.vcs.track: - if drawn.is_directory and drawn.vcs.is_root: - vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcs.remotestatus] + if drawn.vcsremotestatus: + vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcsremotestatus] vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) elif self.target.has_vcschild: vcsstring_display.insert(-1, [" ", []]) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 978171e5..0fbd0113 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -192,8 +192,8 @@ class StatusBar(Widget): left.add(vcsinfo, 'vcsinfo') left.add_space() - if directory.vcs.rootvcs.remotestatus: - vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.rootvcs.remotestatus] + if directory.vcs.rootvcs.obj.vcsremotestatus: + vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.rootvcs.obj.vcsremotestatus] left.add(vcsstr.strip(), 'vcsremote', *vcscol) if target.vcsstatus: vcsstr, vcscol = self.vcsstatus_symb[target.vcsstatus] -- cgit 1.4.1-2-gfad0 From 12fdaa9e4b205e683fc5a8681f5be12c8c825843 Mon Sep 17 00:00:00 2001 From: nfnty Date: Tue, 22 Dec 2015 18:38:34 +0100 Subject: VCS: Fix python2 compatibility --- ranger/ext/vcs/vcs.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 2552d955..6e207b09 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -7,6 +7,10 @@ import os import subprocess import threading import time +try: + FileNotFoundError +except NameError: + FileNotFoundError = OSError class VcsError(Exception): """VCS exception""" @@ -108,16 +112,15 @@ class Vcs(object): def _vcs(self, cmd, path, catchout=True, retbytes=False): """Run a VCS command""" - try: - if catchout: - output = subprocess.check_output(cmd, cwd=path, - stderr=subprocess.DEVNULL) - return output if retbytes else output.decode('UTF-8') - else: - subprocess.check_call(cmd, cwd=path, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except (subprocess.CalledProcessError, FileNotFoundError): - raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) + with open(os.devnull, 'w') as devnull: + try: + if catchout: + output = subprocess.check_output(cmd, cwd=path, stderr=devnull) + return output if retbytes else output.decode('UTF-8') + else: + subprocess.check_call(cmd, cwd=path, stdout=devnull, stderr=devnull) + except (subprocess.CalledProcessError, FileNotFoundError): + raise VcsError('{0:s}: {1:s}'.format(str(cmd), path)) def _get_repotype(self, path): """Get type for path""" -- cgit 1.4.1-2-gfad0 From e7cde84822cf990501e2fbba60e3109829af7aa2 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 28 Dec 2015 00:10:51 +0100 Subject: VCS: Initialize roots in VcsThread --- ranger/config/commands.py | 4 +- ranger/container/directory.py | 12 +-- ranger/ext/vcs/vcs.py | 199 +++++++++++++++++++++++++++--------------- 3 files changed, 134 insertions(+), 81 deletions(-) diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 4197ca7b..04b71f8a 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1364,7 +1364,7 @@ class stage(Command): self.fm.thisdir.vcs.action_add(filelist) except VcsError as error: self.fm.notify('Unable to stage files: {0:s}'.format(str(error))) - self.fm.ui.vcsthread.wakeup() + self.fm.ui.vcsthread.wakeup(self.fm.thisdir) else: self.fm.notify('Unable to stage files: Not in repository') @@ -1383,7 +1383,7 @@ class unstage(Command): self.fm.thisdir.vcs.action_reset(filelist) except VcsError as error: self.fm.notify('Unable to unstage files: {0:s}'.format(str(error))) - self.fm.ui.vcsthread.wakeup() + self.fm.ui.vcsthread.wakeup(self.fm.thisdir) else: self.fm.notify('Unable to unstage files: Not in repository') diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 9e88d265..cd814cb1 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -332,19 +332,15 @@ class Directory(FileSystemObject, Accumulator, Loadable): else: item.relative_path = item.basename item.relative_path_lower = item.relative_path.lower() - if item.vcs and item.vcs.track and item.is_link: - if os.path.realpath(item.path) == item.vcs.root: - item.vcsstatus = item.vcs.rootvcs.obj.vcsstatus - item.vcsremotestatus = item.vcs.rootvcs.obj.vcsremotestatus - else: - item.vcsstatus = item.vcs.status_subpath(item.path) + if item.vcs and item.vcs.track and not item.vcs.is_root: + item.vcsstatus = item.vcs.rootvcs.status_subpath(item.path) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) item.load() disk_usage += item.size if self.vcs and self.vcs.track: - item.vcsstatus = self.vcs.status_subpath(item.path) + item.vcsstatus = self.vcs.rootvcs.status_subpath(item.path) files.append(item) self.percent = 100 * len(files) // len(filenames) @@ -383,7 +379,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): self.loading = False self.fm.signal_emit("finished_loading_dir", directory=self) if self.vcs: - self.fm.ui.vcsthread.wakeup() + self.fm.ui.vcsthread.wakeup(self) def unload(self): self.loading = False diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 6e207b09..b4009d62 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -7,6 +7,12 @@ import os import subprocess import threading import time + +# Python2 compatibility +try: + import queue +except ImportError: + import Queue as queue try: FileNotFoundError except NameError: @@ -46,8 +52,8 @@ class Vcs(object): 'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn', 'lazy': True}, } - # Possible directory statuses in order of importance with statuses that - # don't make sense disabled + # Possible directory statuses in order of importance + # statuses that should not be inherited from subpaths are disabled DIRSTATUSES = ( 'conflict', 'untracked', @@ -60,52 +66,47 @@ class Vcs(object): 'unknown', ) - def __init__(self, directoryobject): - self.obj = directoryobject - self.path = self.obj.path + def __init__(self, dirobj): + self.obj = dirobj + self.path = dirobj.path self.repotypes_settings = set( repotype for repotype, values in self.REPOTYPES.items() - if getattr(self.obj.settings, values['setting']) in ('enabled', 'local') + if getattr(dirobj.settings, values['setting']) in ('enabled', 'local') ) - self.in_repodir = False - self.track = False self.root, self.repodir, self.repotype, self.links = self._find_root(self.path) self.is_root = True if self.obj.path == self.root else False + self.rootvcs = None + self.rootinit = False + self.head = None + self.branch = None + self.updatetime = None + self.track = False + self.in_repodir = False + self.status_subpaths = None + if self.root: if self.is_root: self.rootvcs = self self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), self.REPOTYPES[self.repotype]['class']) - self.status_subpaths = None if not os.access(self.repodir, os.R_OK): self.obj.vcsremotestatus = 'unknown' self.obj.vcsstatus = 'unknown' return - try: - self.head = self.data_info(self.HEAD) - self.branch = self.data_branch() - self.obj.vcsremotestatus = self.data_status_remote() - self.obj.vcsstatus = self.data_status_root() - except VcsError: - return - - self.timestamp = time.time() self.track = True else: - self.rootvcs = directoryobject.fm.get_directory(self.root).vcs + self.rootvcs = dirobj.fm.get_directory(self.root).vcs self.rootvcs.links |= self.links self.__class__ = self.rootvcs.__class__ + self.track = self.rootvcs.track - # Do not track self.repodir or its subpaths if self.path == self.repodir or self.path.startswith(self.repodir + '/'): self.in_repodir = True - return - - self.track = self.rootvcs.track + self.track = False # Generic #--------------------------- @@ -125,7 +126,7 @@ class Vcs(object): def _get_repotype(self, path): """Get type for path""" for repotype in self.repotypes_settings: - repodir = os.path.join(path, '.{0:s}'.format(repotype)) + repodir = os.path.join(path, '.' + repotype) if os.path.exists(repodir): return (repodir, repotype) return (None, None) @@ -151,18 +152,31 @@ class Vcs(object): return (None, None, None, None) + def init_root(self): + """Initialize root cheaply""" + try: + self.head = self.data_info(self.HEAD) + self.branch = self.data_branch() + self.obj.vcsremotestatus = self.data_status_remote() + self.obj.vcsstatus = self.data_status_root() + except VcsError: + self.update_tree(purge=True) + return False + self.rootinit = True + return True + def _update_walk(self, path, purge): """Update walk""" for wroot, wdirs, _ in os.walk(path): # Only update loaded directories try: - wroot_obj = self.obj.fm.directories[wroot] + wrootobj = self.obj.fm.directories[wroot] except KeyError: wdirs[:] = [] continue - if wroot_obj.content_loaded: + if wrootobj.content_loaded: has_vcschild = False - for fileobj in wroot_obj.files_all: + for fileobj in wrootobj.files_all: if purge: if fileobj.is_directory: fileobj.vcsstatus = None @@ -178,11 +192,11 @@ class Vcs(object): if fileobj.vcs.is_root: has_vcschild = True else: - fileobj.vcsstatus = wroot_obj.vcs.status_subpath( + fileobj.vcsstatus = self.status_subpath( fileobj.path, is_directory=True) else: - fileobj.vcsstatus = wroot_obj.vcs.status_subpath(fileobj.path) - wroot_obj.has_vcschild = has_vcschild + fileobj.vcsstatus = self.status_subpath(fileobj.path) + wrootobj.has_vcschild = has_vcschild # Remove dead directories for wdir in list(wdirs): @@ -196,35 +210,39 @@ class Vcs(object): def update_tree(self, purge=False): """Update tree state""" - self._update_walk(self.root, purge) - for path in list(self.rootvcs.links): + self._update_walk(self.path, purge) + for path in list(self.links): self._update_walk(path, purge) try: dirobj = self.obj.fm.directories[path] except KeyError: + self.links.remove(path) continue if purge: dirobj.vcsstatus = None dirobj.vcs.__init__(dirobj) - elif dirobj.vcs.path == self.root: - dirobj.vcsstatus = self.rootvcs.status_root() + elif dirobj.vcs.path == self.path: + dirobj.vcsremotestatus = self.obj.vcsremotestatus + dirobj.vcsstatus = self.obj.vcsstatus else: - dirobj.vcsstatus = dirobj.vcs.status_subpath(dirobj.path, is_directory=True) + dirobj.vcsstatus = self.status_subpath( + os.path.realpath(dirobj.path), is_directory=True) if purge: - self.rootvcs.__init__(self.rootvcs.obj) + self.__init__(self.obj) def update_root(self): """Update root state""" try: - self.rootvcs.head = self.rootvcs.data_info(self.HEAD) - self.rootvcs.branch = self.rootvcs.data_branch() - self.rootvcs.status_subpaths = self.rootvcs.data_status_subpaths() - self.rootvcs.obj.vcsremotestatus = self.rootvcs.data_status_remote() - self.rootvcs.obj.vcsstatus = self.rootvcs.status_root() + self.head = self.data_info(self.HEAD) + self.branch = self.data_branch() + self.status_subpaths = self.data_status_subpaths() + self.obj.vcsremotestatus = self.data_status_remote() + self.obj.vcsstatus = self.status_root() except VcsError: self.update_tree(purge=True) return False - self.timestamp = time.time() + self.rootinit = True + self.updatetime = time.time() return True def check(self): @@ -233,12 +251,15 @@ class Vcs(object): and (not self.track or (not self.is_root and self._get_repotype(self.path)[0])): self.__init__(self.obj) elif self.track and not os.path.exists(self.repodir): - self.update_tree(purge=True) + self.rootvcs.update_tree(purge=True) return False return True def check_outdated(self): - """Check if outdated""" + """Check if root is outdated""" + if self.updatetime is None: + return True + for wroot, wdirs, _ in os.walk(self.path): wrootobj = self.obj.fm.get_directory(wroot) wrootobj.load_if_outdated() @@ -246,18 +267,18 @@ class Vcs(object): wdirs[:] = [] continue - if wrootobj.stat and self.timestamp < wrootobj.stat.st_mtime: + if wrootobj.stat and self.updatetime < 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: + if wfile.stat and self.updatetime < wfile.stat.st_mtime: return True return False def status_root(self): """Returns root status""" - if self.rootvcs.status_subpaths is None: - return 'unknown' + if self.status_subpaths is None: + return 'none' statuses = set(status for path, status in self.status_subpaths.items()) for status in self.DIRSTATUSES: @@ -271,27 +292,21 @@ class Vcs(object): path needs to be self.obj.path or subpath thereof """ - if self.rootvcs.status_subpaths is None: - return 'unknown' + if self.status_subpaths is None: + return 'none' - if path == self.obj.path: - relpath = os.path.relpath(self.path, self.root) - else: - relpath = os.path.relpath( - os.path.join(self.path, os.path.relpath(path, self.obj.path)), - self.root, - ) + relpath = os.path.relpath(path, self.path) # check if relpath or its parents has a status tmppath = relpath while tmppath: - if tmppath in self.rootvcs.status_subpaths: - return self.rootvcs.status_subpaths[tmppath] + if tmppath in self.status_subpaths: + return self.status_subpaths[tmppath] tmppath = os.path.dirname(tmppath) # check if path contains some file in status if is_directory: - statuses = set(status for subpath, status in self.rootvcs.status_subpaths.items() + statuses = set(status for subpath, status in self.status_subpaths.items() if subpath.startswith(relpath + '/')) for status in self.DIRSTATUSES: if status in statuses: @@ -336,14 +351,37 @@ class Vcs(object): """Returns info string about revision rev. None in special cases""" raise NotImplementedError +def init_subroots(dirobj): + """Initialize roots under dirobj""" + redraw = False + for fileobj in dirobj.files_all: + if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: + continue + if fileobj.vcs.is_root and not fileobj.vcs.rootinit: + if fileobj.vcs.init_root(): + redraw = True + elif fileobj.is_link: + if os.path.realpath(fileobj.path) == fileobj.vcs.root: + if not fileobj.vcs.rootvcs.rootinit: + fileobj.vcs.rootvcs.init_root() + fileobj.vcsstatus = fileobj.vcs.rootvcs.obj.vcsstatus + fileobj.vcsremotestatus = fileobj.vcs.rootvcs.obj.vcsremotestatus + else: + fileobj.vcsstatus = fileobj.vcs.rootvcs.status_subpath( + os.path.realpath(fileobj.path)) + redraw = True + return redraw + class VcsThread(threading.Thread): - """Vcs thread""" + """VCS thread""" def __init__(self, ui, idle_delay): super(VcsThread, self).__init__() self.daemon = True self.ui = ui self.delay = idle_delay + self.queue = queue.Queue() self.wake = threading.Event() + self.awoken = False def _check(self): """Check for hinders""" @@ -365,14 +403,31 @@ class VcsThread(threading.Thread): return None def run(self): - roots = set() # already updated roots + roots = set() # Handled roots redraw = False + while True: if self._check(): self.wake.wait(timeout=self.delay) - self.wake.clear() + if self.wake.is_set(): + self.awoken = True + self.wake.clear() continue + while True: + try: + dirobj = self.queue.get(block=False) + except queue.Empty: + break + # Update if root + if dirobj.vcs.track and dirobj.vcs.is_root: + roots.add(dirobj.vcs.path) + if dirobj.vcs.update_root(): + dirobj.vcs.update_tree() + redraw = True + if dirobj.files_all and init_subroots(dirobj): + redraw = True + # Exclude root if repodir in the rightmost column (causes strobing) target = self._targeted_directory_rightmost() if target and target.vcs and target.vcs.in_repodir: @@ -387,11 +442,9 @@ class VcsThread(threading.Thread): if target.vcs.track and target.vcs.root not in roots: roots.add(target.vcs.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() + if ((lazy and target.vcs.rootvcs.check_outdated()) or not lazy) \ + and target.vcs.rootvcs.update_root(): + target.vcs.rootvcs.update_tree() redraw = True roots.clear() @@ -401,14 +454,18 @@ class VcsThread(threading.Thread): if column.target and column.target.is_directory: column.need_redraw = True self.ui.status.need_redraw = True - if self.wake.is_set(): + if self.awoken: self.ui.redraw() - self.wake.clear() + self.awoken = False self.wake.wait(timeout=self.delay) + if self.wake.is_set(): + self.awoken = True + self.wake.clear() - def wakeup(self): + def wakeup(self, dirobj): """Wakeup thread""" + self.queue.put(dirobj) self.wake.set() # Backend imports -- cgit 1.4.1-2-gfad0 From 69ac2c873254f82669471254ee6b174237957738 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 28 Dec 2015 11:27:35 +0100 Subject: VCS: Fix browsercolumn --- ranger/ext/vcs/vcs.py | 15 ++++++++++++--- ranger/gui/widgets/__init__.py | 28 ++++++++++++++-------------- ranger/gui/widgets/browsercolumn.py | 14 +++++++------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index b4009d62..d70af92d 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -354,14 +354,18 @@ class Vcs(object): def init_subroots(dirobj): """Initialize roots under dirobj""" redraw = False + has_vcschild = False + for fileobj in dirobj.files_all: if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: continue - if fileobj.vcs.is_root and not fileobj.vcs.rootinit: - if fileobj.vcs.init_root(): - redraw = True + if fileobj.vcs.is_root: + has_vcschild = True + if not fileobj.vcs.rootinit: + fileobj.vcs.init_root() elif fileobj.is_link: if os.path.realpath(fileobj.path) == fileobj.vcs.root: + has_vcschild = True if not fileobj.vcs.rootvcs.rootinit: fileobj.vcs.rootvcs.init_root() fileobj.vcsstatus = fileobj.vcs.rootvcs.obj.vcsstatus @@ -370,6 +374,11 @@ def init_subroots(dirobj): fileobj.vcsstatus = fileobj.vcs.rootvcs.status_subpath( os.path.realpath(fileobj.path)) redraw = True + + if dirobj.has_vcschild != has_vcschild: + redraw = True + dirobj.has_vcschild = has_vcschild + return redraw class VcsThread(threading.Thread): diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py index ca3e90ce..f61e18eb 100644 --- a/ranger/gui/widgets/__init__.py +++ b/ranger/gui/widgets/__init__.py @@ -6,24 +6,24 @@ class Widget(Displayable): """A class for classification of widgets.""" vcsstatus_symb = { - 'conflict': ('X', ["vcsconflict"]), - 'untracked': ('+', ["vcschanged"]), - 'deleted': ('-', ["vcschanged"]), - 'changed': ('*', ["vcschanged"]), - 'staged': ('*', ["vcsstaged"]), - 'ignored': ('·', ["vcsignored"]), - 'sync': ('√', ["vcssync"]), + 'conflict': ('X', ['vcsconflict']), + 'untracked': ('+', ['vcschanged']), + 'deleted': ('-', ['vcschanged']), + 'changed': ('*', ['vcschanged']), + 'staged': ('*', ['vcsstaged']), + 'ignored': ('·', ['vcsignored']), + 'sync': ('√', ['vcssync']), 'none': (' ', []), - 'unknown': ('?', ["vcsunknown"]), + 'unknown': ('?', ['vcsunknown']), } vcsremotestatus_symb = { - 'diverged': ('Y', ["vcsdiverged"]), - 'ahead': ('>', ["vcsahead"]), - 'behind': ('<', ["vcsbehind"]), - 'sync': ('=', ["vcssync"]), - 'none': ('⌂', ["vcsnone"]), - 'unknown': ('?', ["vcsunknown"]), + 'diverged': ('Y', ['vcsdiverged']), + 'ahead': ('>', ['vcsahead']), + 'behind': ('<', ['vcsbehind']), + 'sync': ('=', ['vcssync']), + 'none': ('⌂', ['vcsnone']), + 'unknown': ('?', ['vcsunknown']), } ellipsis = {False: '~', True: '…'} diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 944f5c58..07830b31 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -257,7 +257,7 @@ class BrowserColumn(Pager): metakey = hash(repr(sorted(metadata.items()))) if metadata else 0 key = (self.wid, selected_i == i, drawn.marked, self.main_column, drawn.path in copied, tagged_marker, drawn.infostring, - drawn.vcsstatus, drawn.vcsremotestatus, + drawn.vcsstatus, drawn.vcsremotestatus, self.target.has_vcschild, self.fm.do_cut, current_linemode.name, metakey) if key in drawn.display_data: @@ -372,20 +372,20 @@ class BrowserColumn(Pager): def _draw_vcsstring_display(self, drawn): vcsstring_display = [] - directory = drawn if drawn.is_directory else self.target - if directory.vcs and directory.vcs.track: + if (self.target.vcs and self.target.vcs.track) \ + or (drawn.is_directory and drawn.vcs and drawn.vcs.track): if drawn.vcsremotestatus: vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcsremotestatus] vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol]) elif self.target.has_vcschild: - vcsstring_display.insert(-1, [" ", []]) + vcsstring_display.append([' ', []]) if drawn.vcsstatus: vcsstr, vcscol = self.vcsstatus_symb[drawn.vcsstatus] vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol]) - else: - vcsstring_display.append([" ", []]) + elif self.target.has_vcschild: + vcsstring_display.append([' ', []]) elif self.target.has_vcschild: - vcsstring_display.append([" ", []]) + vcsstring_display.append([' ', []]) return vcsstring_display -- cgit 1.4.1-2-gfad0 From d75ecbd7ea4139cbda1205bcd78525dc29f983a1 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 28 Dec 2015 13:24:32 +0100 Subject: VCS: Refactor VcsThread.run() --- ranger/ext/vcs/vcs.py | 169 +++++++++++++++++++++++++++----------------------- 1 file changed, 90 insertions(+), 79 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index d70af92d..4edd53b0 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -100,6 +100,9 @@ class Vcs(object): self.track = True else: self.rootvcs = dirobj.fm.get_directory(self.root).vcs + self.rootvcs.check() + if self.rootvcs.root is None: + return self.rootvcs.links |= self.links self.__class__ = self.rootvcs.__class__ self.track = self.rootvcs.track @@ -250,6 +253,7 @@ class Vcs(object): if not self.in_repodir \ and (not self.track or (not self.is_root and self._get_repotype(self.path)[0])): self.__init__(self.obj) + return False elif self.track and not os.path.exists(self.repodir): self.rootvcs.update_tree(purge=True) return False @@ -351,36 +355,6 @@ class Vcs(object): """Returns info string about revision rev. None in special cases""" raise NotImplementedError -def init_subroots(dirobj): - """Initialize roots under dirobj""" - redraw = False - has_vcschild = False - - for fileobj in dirobj.files_all: - if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: - continue - if fileobj.vcs.is_root: - has_vcschild = True - if not fileobj.vcs.rootinit: - fileobj.vcs.init_root() - elif fileobj.is_link: - if os.path.realpath(fileobj.path) == fileobj.vcs.root: - has_vcschild = True - if not fileobj.vcs.rootvcs.rootinit: - fileobj.vcs.rootvcs.init_root() - fileobj.vcsstatus = fileobj.vcs.rootvcs.obj.vcsstatus - fileobj.vcsremotestatus = fileobj.vcs.rootvcs.obj.vcsremotestatus - else: - fileobj.vcsstatus = fileobj.vcs.rootvcs.status_subpath( - os.path.realpath(fileobj.path)) - redraw = True - - if dirobj.has_vcschild != has_vcschild: - redraw = True - dirobj.has_vcschild = has_vcschild - - return redraw - class VcsThread(threading.Thread): """VCS thread""" def __init__(self, ui, idle_delay): @@ -391,8 +365,11 @@ class VcsThread(threading.Thread): self.queue = queue.Queue() self.wake = threading.Event() self.awoken = False + self.timestamp = time.time() + self.redraw = False + self.roots = set() - def _check(self): + def _hindered(self): """Check for hinders""" for column in self.ui.browser.columns: if column.target and column.target.is_directory and column.target.flat: @@ -411,54 +388,91 @@ class VcsThread(threading.Thread): return target return None - def run(self): - roots = set() # Handled roots - redraw = False + def _queue_process(self): + """Process queue: Initialize roots under dirobj""" while True: - if self._check(): - self.wake.wait(timeout=self.delay) - if self.wake.is_set(): - self.awoken = True - self.wake.clear() + try: + dirobj = self.queue.get(block=False) + except queue.Empty: + break + + # Update if root + if dirobj.vcs.track and dirobj.vcs.is_root: + self.roots.add(dirobj.vcs.path) + if dirobj.vcs.update_root(): + dirobj.vcs.update_tree() + self.redraw = True + + if dirobj.files_all is None: continue - while True: - try: - dirobj = self.queue.get(block=False) - except queue.Empty: - break - # Update if root - if dirobj.vcs.track and dirobj.vcs.is_root: - roots.add(dirobj.vcs.path) - if dirobj.vcs.update_root(): - dirobj.vcs.update_tree() - redraw = True - if dirobj.files_all and init_subroots(dirobj): - redraw = True - - # Exclude root if repodir in the rightmost column (causes strobing) - target = self._targeted_directory_rightmost() - if target and target.vcs and target.vcs.in_repodir: - roots.add(target.vcs.root) - - for column in self.ui.browser.columns: - target = column.target - if target and target.is_directory and target.vcs: - # Redraw if tree is purged - if not target.vcs.check(): - redraw = True - if target.vcs.track and target.vcs.root not in roots: - roots.add(target.vcs.root) - lazy = target.vcs.REPOTYPES[target.vcs.repotype]['lazy'] - if ((lazy and target.vcs.rootvcs.check_outdated()) or not lazy) \ - and target.vcs.rootvcs.update_root(): - target.vcs.rootvcs.update_tree() - redraw = True - roots.clear() - - if redraw: - redraw = False + has_vcschild = False + for fileobj in dirobj.files_all: + if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: + continue + if fileobj.vcs.is_root: + has_vcschild = True + if not fileobj.vcs.rootinit: + fileobj.vcs.init_root() + self.redraw = True + elif fileobj.is_link: + if os.path.realpath(fileobj.path) == fileobj.vcs.root: + has_vcschild = True + if not fileobj.vcs.rootvcs.rootinit: + fileobj.vcs.rootvcs.init_root() + fileobj.vcsstatus = fileobj.vcs.rootvcs.obj.vcsstatus + fileobj.vcsremotestatus = fileobj.vcs.rootvcs.obj.vcsremotestatus + else: + fileobj.vcsstatus = fileobj.vcs.rootvcs.status_subpath( + os.path.realpath(fileobj.path)) + self.redraw = True + + if dirobj.has_vcschild != has_vcschild: + self.redraw = True + dirobj.has_vcschild = has_vcschild + + def _update_columns(self): + """Update targeted directories""" + for column in self.ui.browser.columns: + target = column.target + if target and target.is_directory and target.vcs: + # Redraw if tree is purged + if not target.vcs.check(): + self.redraw = True + + if target.vcs.track and target.vcs.root not in self.roots: + self.roots.add(target.vcs.root) + lazy = target.vcs.REPOTYPES[target.vcs.repotype]['lazy'] + if ((lazy and target.vcs.rootvcs.check_outdated()) or not lazy) \ + and target.vcs.rootvcs.update_root(): + target.vcs.rootvcs.update_tree() + self.redraw = True + + def run(self): + while True: + curtime = time.time() + if self.wake.wait(timeout=((self.timestamp + self.delay) - curtime)): + self.awoken = True + self.wake.clear() + + if self._hindered(): + continue + + if self.awoken: + self._queue_process() + else: + self.timestamp = curtime + + # Exclude root if repodir in the rightmost column (causes strobing) + target = self._targeted_directory_rightmost() + if target and target.vcs and target.vcs.in_repodir: + self.roots.add(target.vcs.root) + + self._update_columns() + + if self.redraw: + self.redraw = False for column in self.ui.browser.columns: if column.target and column.target.is_directory: column.need_redraw = True @@ -466,11 +480,8 @@ class VcsThread(threading.Thread): if self.awoken: self.ui.redraw() + self.roots.clear() self.awoken = False - self.wake.wait(timeout=self.delay) - if self.wake.is_set(): - self.awoken = True - self.wake.clear() def wakeup(self, dirobj): """Wakeup thread""" -- cgit 1.4.1-2-gfad0 From 1b97f7150f50b610c9cba20da4e4d89e27e61374 Mon Sep 17 00:00:00 2001 From: nfnty Date: Fri, 1 Jan 2016 00:48:07 +0100 Subject: VCS: pylint and flake8 compliancy --- ranger/ext/vcs/__init__.py | 2 +- ranger/ext/vcs/bzr.py | 4 +--- ranger/ext/vcs/git.py | 6 ++---- ranger/ext/vcs/vcs.py | 24 ++++++++++++------------ 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index 36bbb369..ec26f07a 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -3,4 +3,4 @@ """VCS Extension""" -from .vcs import Vcs, VcsError, VcsThread +from .vcs import Vcs, VcsError, VcsThread # NOQA diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index b06f28ec..300db796 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -8,6 +8,7 @@ import re from datetime import datetime from .vcs import Vcs, VcsError + class Bzr(Vcs): """VCS implementation for GNU Bazaar""" HEAD = 'last:1' @@ -19,7 +20,6 @@ class Bzr(Vcs): ) # Generic - #--------------------------- def _bzr(self, args, path=None, catchout=True, retbytes=False): """Run a bzr command""" @@ -71,7 +71,6 @@ class Bzr(Vcs): return 'unknown' # Action Interface - #--------------------------- def action_add(self, filelist=None): args = ['add'] @@ -86,7 +85,6 @@ class Bzr(Vcs): self._bzr(args, catchout=False) # Data Interface - #--------------------------- def data_status_root(self): statuses = set() diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 1741ceff..5007b877 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -9,6 +9,7 @@ from datetime import datetime import json from .vcs import Vcs, VcsError + class Git(Vcs): """VCS implementation for Git""" _status_translations = ( @@ -25,7 +26,6 @@ class Git(Vcs): ) # Generic - #--------------------------- def _git(self, args, path=None, catchout=True, retbytes=False): """Run a git command""" @@ -75,13 +75,12 @@ class Git(Vcs): def _git_status_translate(self, code): """Translate status code""" - for X, Y, status in self._status_translations: + for X, Y, status in self._status_translations: # pylint: disable=invalid-name if code[0] in X and code[1] in Y: return status return 'unknown' # Action interface - #--------------------------- def action_add(self, filelist=None): args = ['add', '--all'] @@ -96,7 +95,6 @@ class Git(Vcs): self._git(args, catchout=False) # Data Interface - #--------------------------- def data_status_root(self): statuses = set() diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 4edd53b0..f4e0f378 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -12,17 +12,19 @@ import time try: import queue except ImportError: - import Queue as queue + import Queue as queue # pylint: disable=import-error try: FileNotFoundError except NameError: - FileNotFoundError = OSError + FileNotFoundError = OSError # pylint: disable=redefined-builtin + class VcsError(Exception): """VCS exception""" pass -class Vcs(object): + +class Vcs(object): # pylint: disable=too-many-instance-attributes """ This class represents a version controlled path, abstracting the usual operations from the different supported backends. @@ -112,9 +114,8 @@ class Vcs(object): self.track = False # Generic - #--------------------------- - def _vcs(self, cmd, path, catchout=True, retbytes=False): + def _vcs(self, cmd, path, catchout=True, retbytes=False): # pylint: disable=no-self-use """Run a VCS command""" with open(os.devnull, 'w') as devnull: try: @@ -318,7 +319,6 @@ class Vcs(object): return 'sync' # Action interface - #--------------------------- def action_add(self, filelist): """Adds files to the index""" @@ -329,7 +329,6 @@ class Vcs(object): raise NotImplementedError # Data interface - #--------------------------- def data_status_root(self): """Returns status of self.root cheaply""" @@ -355,12 +354,13 @@ class Vcs(object): """Returns info string about revision rev. None in special cases""" raise NotImplementedError + class VcsThread(threading.Thread): """VCS thread""" def __init__(self, ui, idle_delay): super(VcsThread, self).__init__() self.daemon = True - self.ui = ui + self.ui = ui # pylint: disable=invalid-name self.delay = idle_delay self.queue = queue.Queue() self.wake = threading.Event() @@ -489,7 +489,7 @@ class VcsThread(threading.Thread): self.wake.set() # Backend imports -import ranger.ext.vcs.git -import ranger.ext.vcs.hg -import ranger.ext.vcs.bzr -import ranger.ext.vcs.svn +import ranger.ext.vcs.git # NOQA pylint: disable=wrong-import-position +import ranger.ext.vcs.hg # NOQA pylint: disable=wrong-import-position +import ranger.ext.vcs.bzr # NOQA pylint: disable=wrong-import-position +import ranger.ext.vcs.svn # NOQA pylint: disable=wrong-import-position -- cgit 1.4.1-2-gfad0 From 2753e22e6ba3a17482709747be2de2eb6a6a719c Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 7 Feb 2016 23:33:10 +0100 Subject: VCS: Minor fixes --- ranger/ext/vcs/__init__.py | 4 +++- ranger/ext/vcs/bzr.py | 14 +++++++------- ranger/ext/vcs/git.py | 22 ++++++++++------------ ranger/ext/vcs/hg.py | 1 + ranger/ext/vcs/svn.py | 1 + ranger/ext/vcs/vcs.py | 6 +++--- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py index ec26f07a..26a32800 100644 --- a/ranger/ext/vcs/__init__.py +++ b/ranger/ext/vcs/__init__.py @@ -3,4 +3,6 @@ """VCS Extension""" -from .vcs import Vcs, VcsError, VcsThread # NOQA +from .vcs import Vcs, VcsError, VcsThread + +__all__ = ['Vcs', 'VcsError', 'VcsThread'] diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 300db796..11461f97 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -6,6 +6,7 @@ import os import re from datetime import datetime + from .vcs import Vcs, VcsError @@ -26,9 +27,9 @@ class Bzr(Vcs): return self._vcs(['bzr'] + args, path or self.path, catchout=catchout, retbytes=retbytes) def _remote_url(self): - """Returns remote url""" + """Remote url""" try: - return self._bzr(['config', 'parent_location']).rstrip() or None + return self._bzr(['config', 'parent_location']).rstrip('\n') or None except VcsError: return None @@ -65,8 +66,8 @@ class Bzr(Vcs): 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: + for code_x, code_y, status in self._status_translations: + if code[0] in code_x and code[1] in code_y: return status return 'unknown' @@ -114,12 +115,11 @@ class Bzr(Vcs): def data_status_remote(self): if not self._remote_url(): return 'none' - return 'unknown' def data_branch(self): try: - return self._bzr(['nick']).rstrip() or None + return self._bzr(['nick']).rstrip('\n') or None except VcsError: return None @@ -136,4 +136,4 @@ class Bzr(Vcs): elif len(log) == 1: return log[0] else: - raise VcsError('More than one instance of revision {0:s} ?!?'.format(rev)) + raise VcsError('More than one instance of revision {0:s}'.format(rev)) diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 5007b877..e64055a1 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -7,6 +7,7 @@ import os import re from datetime import datetime import json + from .vcs import Vcs, VcsError @@ -33,13 +34,13 @@ class Git(Vcs): def _head_ref(self): """Returns HEAD reference""" - return self._git(['symbolic-ref', self.HEAD]).rstrip() or None + return self._git(['symbolic-ref', self.HEAD]).rstrip('\n') or None def _remote_ref(self, ref): """Returns remote reference associated to given ref""" if ref is None: return None - return self._git(['for-each-ref', '--format=%(upstream)', ref]).rstrip() or None + return self._git(['for-each-ref', '--format=%(upstream)', ref]).rstrip('\n') or None def _log(self, refspec=None, maxres=None, filelist=None): """Returns an array of dicts containing revision info for refspec""" @@ -73,10 +74,10 @@ class Git(Vcs): log.append(line) return log - def _git_status_translate(self, code): + def _status_translate(self, code): """Translate status code""" - for X, Y, status in self._status_translations: # pylint: disable=invalid-name - if code[0] in X and code[1] in Y: + for code_x, code_y, status in self._status_translations: + if code[0] in code_x and code[1] in code_y: return status return 'unknown' @@ -105,7 +106,7 @@ class Git(Vcs): if skip: skip = False continue - statuses.add(self._git_status_translate(line[:2])) + statuses.add(self._status_translate(line[:2])) if line.startswith('R'): skip = True @@ -136,7 +137,7 @@ class Git(Vcs): if skip: skip = False continue - statuses[os.path.normpath(line[3:])] = self._git_status_translate(line[:2]) + statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2]) if line.startswith('R'): skip = True @@ -168,10 +169,7 @@ class Git(Vcs): return 'detached' match = re.match('refs/heads/([^/]+)', head) - if match: - return match.group(1) - else: - return None + return match.group(1) if match else None def data_info(self, rev=None): if rev is None: @@ -186,4 +184,4 @@ class Git(Vcs): elif len(log) == 1: return log[0] else: - raise VcsError('More than one instance of revision {0:s} ?!?'.format(rev)) + raise VcsError('More than one instance of revision {0:s}'.format(rev)) diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 39a81d57..02d8e684 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -6,6 +6,7 @@ import os import re from datetime import datetime + from .vcs import Vcs, VcsError class Hg(Vcs): diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index a8ee66f3..c152ca7c 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -8,6 +8,7 @@ import os import re import logging from datetime import datetime + from .vcs import Vcs, VcsError class SVN(Vcs): diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index f4e0f378..93db5eab 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -169,7 +169,7 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes self.rootinit = True return True - def _update_walk(self, path, purge): + def _update_walk(self, path, purge): # pylint: disable=too-many-branches """Update walk""" for wroot, wdirs, _ in os.walk(path): # Only update loaded directories @@ -355,7 +355,7 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes raise NotImplementedError -class VcsThread(threading.Thread): +class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attributes """VCS thread""" def __init__(self, ui, idle_delay): super(VcsThread, self).__init__() @@ -388,7 +388,7 @@ class VcsThread(threading.Thread): return target return None - def _queue_process(self): + def _queue_process(self): # pylint: disable=too-many-branches """Process queue: Initialize roots under dirobj""" while True: -- cgit 1.4.1-2-gfad0 From af8c2f6a1942011146b62f24c1cc42570a80cd97 Mon Sep 17 00:00:00 2001 From: nfnty Date: Sun, 7 Feb 2016 23:41:15 +0100 Subject: VCS: Improve null handling --- ranger/ext/vcs/bzr.py | 2 +- ranger/ext/vcs/git.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 11461f97..f39ac00f 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -103,7 +103,7 @@ class Bzr(Vcs): statuses = {} # Ignored - for path in self._bzr(['ls', '--null', '--ignored']).split('\x00')[:-1]: + for path in self._bzr(['ls', '--null', '--ignored']).rstrip('\x00').split('\x00'): statuses[path] = 'ignored' # Paths with status diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index e64055a1..e50ed3de 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -102,7 +102,7 @@ class Git(Vcs): # Paths with status skip = False - for line in self._git(['status', '--porcelain', '-z']).split('\x00')[:-1]: + for line in self._git(['status', '--porcelain', '-z']).rstrip('\x00').split('\x00'): if skip: skip = False continue @@ -121,19 +121,20 @@ class Git(Vcs): # Ignored directories for path in self._git( ['ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'])\ - .split('\x00')[:-1]: + .rstrip('\x00').split('\x00'): if path.endswith('/'): statuses[os.path.normpath(path)] = 'ignored' # Empty directories for path in self._git(['ls-files', '-z', '--others', '--directory', '--exclude-standard'])\ - .split('\x00')[:-1]: + .rstrip('\x00').split('\x00'): if path.endswith('/'): statuses[os.path.normpath(path)] = 'none' # Paths with status skip = False - for line in self._git(['status', '--porcelain', '-z', '--ignored']).split('\x00')[:-1]: + for line in self._git(['status', '--porcelain', '-z', '--ignored'])\ + .rstrip('\x00').split('\x00'): if skip: skip = False continue -- cgit 1.4.1-2-gfad0 From 8a43e6ca153768555cf75079f36137df2be1a6c1 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 01:00:30 +0100 Subject: VCS: Implement Mercurial --- ranger/ext/vcs/hg.py | 198 +++++++++++++++++++++++++++++---------------------- 1 file changed, 111 insertions(+), 87 deletions(-) diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 02d8e684..92ca583a 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -4,132 +4,156 @@ """Mercurial module""" import os -import re from datetime import datetime +import json from .vcs import Vcs, VcsError + class Hg(Vcs): """VCS implementation for Mercurial""" HEAD = 'tip' - # Generic - #--------------------------- - - def _hg(self, path, args, silent=True, catchout=False, retbytes=False): - return self._vcs(path, 'hg', args, silent=silent, catchout=catchout, retbytes=retbytes) + _status_translations = ( + ('A', 'staged'), + ('M', 'changed'), + ('R!', 'deleted'), - def _has_head(self): - """Checks whether repo has head""" - rnum = self._hg(self.path, ['-q', 'identify', '--num', '-r', self.HEAD], catchout=True) - return rnum != '-1' + ('?', 'untracked'), + ('I', 'ignored'), + ) - def _sanitize_rev(self, rev): - if rev == None: return None - rev = rev.strip() - if len(rev) == 0: return None - if rev[-1] == '+': rev = rev[:-1] - - try: - if int(rev) == 0: return None - except: - pass + # Generic - return rev + def _hg(self, args, path=None, catchout=True, retbytes=False): + """Run a hg command""" + return self._vcs(['hg'] + args, path or self.path, catchout=catchout, retbytes=retbytes) def _log(self, refspec=None, maxres=None, filelist=None): - fmt = "changeset: {rev}:{node}\ntag: {tags}\nuser: {author}\ndate: {date}\nsummary: {desc}\n" - args = ['log', '--template', fmt] - - if refspec: args = args + ['--limit', '1', '-r', refspec] - elif maxres: args = args + ['--limit', str(maxres)] + args = [ + 'log', '--template', + '\\{' + '\\x00short\\x00:\\x00{rev}\\x00,' + '\\x00revid\\x00:\\x00{node}\\x00,' + '\\x00author\\x00:\\x00{author}\\x00,' + '\\x00date\\x00:\\x00{date}\\x00,' + '\\x00summary\\x00:\\x00{desc}\\x00' + '}\\n' + ] + if refspec: + args += ['--limit', '1', '--rev', refspec] + elif maxres: + args += ['--limit', str(maxres)] + if filelist: + args += ['--'] + filelist - if filelist: args = args + filelist - - raw = self._hg(self.path, args, catchout=True) - L = re.findall('^changeset:\s*([0-9]*):([0-9a-zA-Z]*)\s*$\s*^tag:\s*(.*)\s*$\s*^user:\s*(.*)\s*$\s*^date:\s*(.*)$\s*^summary:\s*(.*)\s*$', raw, re.MULTILINE) + try: + output = self._hg(args)\ + .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines() + except VcsError: + return None log = [] - for t in L: - dt = {} - dt['short'] = t[0].strip() - dt['revid'] = self._sanitize_rev(t[1].strip()) - dt['author'] = t[3].strip() - m = re.match('\d+(\.\d+)?', t[4].strip()) - dt['date'] = datetime.fromtimestamp(float(m.group(0))) - dt['summary'] = t[5].strip() - log.append(dt) + for line in output: + line = json.loads(line) + line['date'] = datetime.fromtimestamp(float(line['date'].split('-')[0])) + log.append(line) return log - def _hg_file_status(self, st): - if len(st) != 1: raise VcsError("Wrong hg file status string: %s" % st) - if st in "ARM": return 'staged' - elif st in "!": return 'deleted' - elif st in "I": return 'ignored' - elif st in "?": return 'untracked' - elif st in "X": return 'conflict' - elif st in "C": return 'sync' - else: return 'unknown' + def _remote_url(self): + """Remote url""" + try: + return self._hg(['showconfig', 'paths.default']).rstrip('\n') or None + except VcsError: + return None + + def _status_translate(self, code): + """Translate status code""" + for code_x, status in self._status_translations: # pylint: disable=invalid-name + if code in code_x: + return status + return 'unknown' - # Action Interface - #--------------------------- + # Action interface def action_add(self, filelist=None): - if filelist != None: self._hg(self.path, ['addremove'] + filelist) - else: self._hg(self.path, ['addremove']) + args = ['add'] + if filelist: + args += ['--'] + filelist + self._hg(args, catchout=False) def action_reset(self, filelist=None): - if filelist == None: filelist = self.data_status_subpaths().keys() - self._hg(self.path, ['forget'] + filelist) + args = ['forget', '--'] + if filelist: + args += filelist + else: + args += self.rootvcs.status_subpaths.keys() + self._hg(args, catchout=False) + + # Data interface + + def data_status_root(self): + statuses = set() - # Data Interface - #--------------------------- + # Paths with status + for line in self._hg(['status', '--all', '--print0']).rstrip('\x00').split('\x00'): + code = line[0] + if code == 'C': + continue + statuses.add(self._status_translate(code)) + + for status in self.DIRSTATUSES: + if status in statuses: + return status + return 'sync' def data_status_subpaths(self): - raw = self._hg(self.path, ['status'], catchout=True, retbytes=True) - L = re.findall('^(.)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) - ret = {} - for st, p in L: - # Detect conflict by the existence of .orig files - if st == '?' and re.match('^.*\.orig\s*$', p): st = 'X' - sta = self._hg_file_status(st) - ret[os.path.normpath(p.strip())] = sta - return ret + statuses = {} + + # Paths with status + for line in self._hg(['status', '--all', '--print0']).rstrip('\x00').split('\x00'): + code, path = line[0], line[2:] + if code == 'C': + continue + statuses[os.path.normpath(path)] = self._status_translate(code) + + return statuses def data_status_remote(self): - if self.get_remote() == None: - return "none" + if self._remote_url() is None: + return 'none' ahead = behind = True try: - self._hg(self.path, ['outgoing'], silent=True) - except: + self._hg(['outgoing'], catchout=False) + except VcsError: ahead = False try: - self._hg(self.path, ['incoming'], silent=True) - except: + self._hg(['incoming'], catchout=False) + except VcsError: behind = False - if ahead and behind: return "diverged" - elif ahead and not behind: return "ahead" - elif not ahead and behind: return "behind" - elif not ahead and not behind: return "sync" + if ahead: + return 'diverged' if behind else 'ahead' + else: + return 'behind' if behind else 'sync' def data_branch(self): - branch = self._hg(self.path, ['branch'], catchout=True) - return branch or None + return self._hg(['branch'], catchout=True).rstrip('\n') or 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 None - - 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)) -- cgit 1.4.1-2-gfad0 From 9c80132600b764ff913c18c1bae9171343680c82 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 01:47:56 +0100 Subject: VCS: Fix output handling --- ranger/ext/vcs/bzr.py | 14 +++++++++---- ranger/ext/vcs/git.py | 54 ++++++++++++++++++++++++++++++--------------------- ranger/ext/vcs/hg.py | 25 +++++++++++++++--------- 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index f39ac00f..3461c3d2 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -91,7 +91,10 @@ class Bzr(Vcs): statuses = set() # Paths with status - for line in self._bzr(['status', '--short', '--no-classify']).splitlines(): + output = self._bzr(['status', '--short', '--no-classify']).rstrip('\n') + if not output: + return 'sync' + for line in output.split('\n'): statuses.add(self._bzr_status_translate(line[:2])) for status in self.DIRSTATUSES: @@ -103,11 +106,14 @@ class Bzr(Vcs): statuses = {} # Ignored - for path in self._bzr(['ls', '--null', '--ignored']).rstrip('\x00').split('\x00'): - statuses[path] = 'ignored' + output = self._bzr(['ls', '--null', '--ignored']).rstrip('\x00') + if output: + for path in output.split('\x00'): + statuses[path] = 'ignored' # Paths with status - for line in self._bzr(['status', '--short', '--no-classify']).splitlines(): + output = self._bzr(['status', '--short', '--no-classify']).rstrip('\n') + for line in output.split('\n'): statuses[os.path.normpath(line[4:])] = self._bzr_status_translate(line[:2]) return statuses diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index e50ed3de..32003d5b 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -62,13 +62,15 @@ class Git(Vcs): args += ['--'] + filelist try: - output = self._git(args)\ - .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines() + output = self._git(args).rstrip('\n') except VcsError: return None + if not output: + return None log = [] - for line in output: + for line in output\ + .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').split('\n'): line = json.loads(line) line['date'] = datetime.fromtimestamp(line['date']) log.append(line) @@ -102,7 +104,10 @@ class Git(Vcs): # Paths with status skip = False - for line in self._git(['status', '--porcelain', '-z']).rstrip('\x00').split('\x00'): + output = self._git(['status', '--porcelain', '-z']).rstrip('\x00') + if not output: + return 'sync' + for line in output.split('\x00'): if skip: skip = False continue @@ -119,28 +124,33 @@ class Git(Vcs): statuses = {} # Ignored directories - for path in self._git( - ['ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'])\ - .rstrip('\x00').split('\x00'): - if path.endswith('/'): - statuses[os.path.normpath(path)] = 'ignored' + output = self._git([ + 'ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard' + ]).rstrip('\x00') + if output: + for path in output.split('\x00'): + if path.endswith('/'): + statuses[os.path.normpath(path)] = 'ignored' # Empty directories - for path in self._git(['ls-files', '-z', '--others', '--directory', '--exclude-standard'])\ - .rstrip('\x00').split('\x00'): - if path.endswith('/'): - statuses[os.path.normpath(path)] = 'none' + output = self._git( + ['ls-files', '-z', '--others', '--directory', '--exclude-standard']).rstrip('\x00') + if output: + for path in output.split('\x00'): + if path.endswith('/'): + statuses[os.path.normpath(path)] = 'none' # Paths with status - skip = False - for line in self._git(['status', '--porcelain', '-z', '--ignored'])\ - .rstrip('\x00').split('\x00'): - if skip: - skip = False - continue - statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2]) - if line.startswith('R'): - skip = True + output = self._git(['status', '--porcelain', '-z', '--ignored']).rstrip('\x00') + if output: + skip = False + for line in output.split('\x00'): + if skip: + skip = False + continue + statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2]) + if line.startswith('R'): + skip = True return statuses diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 92ca583a..7c5507eb 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -49,13 +49,15 @@ class Hg(Vcs): args += ['--'] + filelist try: - output = self._hg(args)\ - .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines() + output = self._hg(args).rstrip('\n') except VcsError: return None + if not output: + return None log = [] - for line in output: + for line in output\ + .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').split('\n'): line = json.loads(line) line['date'] = datetime.fromtimestamp(float(line['date'].split('-')[0])) log.append(line) @@ -97,7 +99,10 @@ class Hg(Vcs): statuses = set() # Paths with status - for line in self._hg(['status', '--all', '--print0']).rstrip('\x00').split('\x00'): + output = self._hg(['status', '--all', '--print0']).rstrip('\x00') + if not output: + return 'sync' + for line in output.split('\x00'): code = line[0] if code == 'C': continue @@ -112,11 +117,13 @@ class Hg(Vcs): statuses = {} # Paths with status - for line in self._hg(['status', '--all', '--print0']).rstrip('\x00').split('\x00'): - code, path = line[0], line[2:] - if code == 'C': - continue - statuses[os.path.normpath(path)] = self._status_translate(code) + output = self._hg(['status', '--all', '--print0']).rstrip('\x00') + if output: + for line in output.split('\x00'): + code, path = line[0], line[2:] + if code == 'C': + continue + statuses[os.path.normpath(path)] = self._status_translate(code) return statuses -- cgit 1.4.1-2-gfad0 From b72ad9ae5a04529865ec154b670771d4f7950bb2 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 02:31:27 +0100 Subject: VCS: Add date to statusbar --- ranger/colorschemes/default.py | 3 +++ ranger/gui/context.py | 2 +- ranger/gui/widgets/statusbar.py | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index 967fa50c..e5dbf3a6 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -100,6 +100,9 @@ class Default(ColorScheme): if context.vcscommit: fg = yellow attr &= ~bold + if context.vcsdate: + fg = cyan + attr &= ~bold if context.text: diff --git a/ranger/gui/context.py b/ranger/gui/context.py index 6b341bf1..ac597e5a 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -17,7 +17,7 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo', 'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded', 'keybuffer', 'infostring', - 'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit', + 'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit', 'vcsdate', 'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored', 'vcsstaged', 'vcssync', 'vcsnone', 'vcsbehind', 'vcsahead', 'vcsdiverged'] diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 0fbd0113..fbd53855 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -200,7 +200,9 @@ class StatusBar(Widget): left.add(vcsstr.strip(), 'vcsfile', *vcscol) if directory.vcs.rootvcs.head: left.add_space() - left.add('{0:s}'.format(directory.vcs.rootvcs.head['summary']), 'vcscommit') + left.add(str(directory.vcs.rootvcs.head['date']), 'vcsdate') + left.add_space() + left.add(directory.vcs.rootvcs.head['summary'], 'vcscommit') def _get_owner(self, target): uid = target.stat.st_uid -- cgit 1.4.1-2-gfad0 From cd47d26afc9d3e53e8bbc5cee9939e3bf2d820da Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 02:49:37 +0100 Subject: VCS: Reorder imports --- ranger/ext/vcs/bzr.py | 2 +- ranger/ext/vcs/git.py | 4 ++-- ranger/ext/vcs/hg.py | 2 +- ranger/ext/vcs/svn.py | 5 +++-- ranger/ext/vcs/vcs.py | 4 ++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index 3461c3d2..ab4b8b06 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -3,9 +3,9 @@ """GNU Bazaar module""" +from datetime import datetime import os import re -from datetime import datetime from .vcs import Vcs, VcsError diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index 32003d5b..ca49998e 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -3,10 +3,10 @@ """Git module""" -import os -import re from datetime import datetime import json +import os +import re from .vcs import Vcs, VcsError diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 7c5507eb..16740307 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -3,9 +3,9 @@ """Mercurial module""" -import os from datetime import datetime import json +import os from .vcs import Vcs, VcsError diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index c152ca7c..14e82db0 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -4,10 +4,11 @@ """Subversion module""" from __future__ import with_statement + +from datetime import datetime +import logging import os import re -import logging -from datetime import datetime from .vcs import Vcs, VcsError diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 93db5eab..a99f9aa9 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -48,9 +48,9 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes # Backends REPOTYPES = { + 'bzr': {'class': 'Bzr', 'setting': 'vcs_backend_bzr', 'lazy': True}, '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}, } @@ -489,7 +489,7 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut self.wake.set() # Backend imports +import ranger.ext.vcs.bzr # NOQA pylint: disable=wrong-import-position import ranger.ext.vcs.git # NOQA pylint: disable=wrong-import-position import ranger.ext.vcs.hg # NOQA pylint: disable=wrong-import-position -import ranger.ext.vcs.bzr # NOQA pylint: disable=wrong-import-position import ranger.ext.vcs.svn # NOQA pylint: disable=wrong-import-position -- cgit 1.4.1-2-gfad0 From cd703e2b5f75af497edbc630539c6cce93070eff Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 02:56:11 +0100 Subject: VCS: hg: Fix statuses --- ranger/ext/vcs/hg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 16740307..f1e9dbdb 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -15,9 +15,9 @@ class Hg(Vcs): HEAD = 'tip' _status_translations = ( - ('A', 'staged'), + ('AR', 'staged'), ('M', 'changed'), - ('R!', 'deleted'), + ('!', 'deleted'), ('?', 'untracked'), ('I', 'ignored'), -- cgit 1.4.1-2-gfad0 From f68763ad11774290279763b30fed37bcf5e0da1a Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 03:15:02 +0100 Subject: VCS: Rename _vcs to _run --- ranger/ext/vcs/bzr.py | 26 +++++++++++--------------- ranger/ext/vcs/git.py | 24 ++++++++++-------------- ranger/ext/vcs/hg.py | 22 +++++++++------------- ranger/ext/vcs/vcs.py | 8 ++++++-- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py index ab4b8b06..5decc8b1 100644 --- a/ranger/ext/vcs/bzr.py +++ b/ranger/ext/vcs/bzr.py @@ -22,14 +22,10 @@ class Bzr(Vcs): # Generic - def _bzr(self, args, path=None, catchout=True, retbytes=False): - """Run a bzr command""" - return self._vcs(['bzr'] + args, path or self.path, catchout=catchout, retbytes=retbytes) - def _remote_url(self): """Remote url""" try: - return self._bzr(['config', 'parent_location']).rstrip('\n') or None + return self._run(['config', 'parent_location']).rstrip('\n') or None except VcsError: return None @@ -42,7 +38,7 @@ class Bzr(Vcs): args += ['--'] + filelist try: - output = self._bzr(args) + output = self._run(args) except VcsError: return None entries = re.findall(r'-+\n(.+?)\n(?:-|\Z)', output, re.MULTILINE | re.DOTALL) @@ -64,7 +60,7 @@ class Bzr(Vcs): log.append(new) return log - def _bzr_status_translate(self, code): + def _status_translate(self, code): """Translate status code""" for code_x, code_y, status in self._status_translations: if code[0] in code_x and code[1] in code_y: @@ -77,13 +73,13 @@ class Bzr(Vcs): args = ['add'] if filelist: args += ['--'] + filelist - self._bzr(args, catchout=False) + self._run(args, catchout=False) def action_reset(self, filelist=None): args = ['remove', '--keep', '--new'] if filelist: args += ['--'] + filelist - self._bzr(args, catchout=False) + self._run(args, catchout=False) # Data Interface @@ -91,11 +87,11 @@ class Bzr(Vcs): statuses = set() # Paths with status - output = self._bzr(['status', '--short', '--no-classify']).rstrip('\n') + output = self._run(['status', '--short', '--no-classify']).rstrip('\n') if not output: return 'sync' for line in output.split('\n'): - statuses.add(self._bzr_status_translate(line[:2])) + statuses.add(self._status_translate(line[:2])) for status in self.DIRSTATUSES: if status in statuses: @@ -106,15 +102,15 @@ class Bzr(Vcs): statuses = {} # Ignored - output = self._bzr(['ls', '--null', '--ignored']).rstrip('\x00') + output = self._run(['ls', '--null', '--ignored']).rstrip('\x00') if output: for path in output.split('\x00'): statuses[path] = 'ignored' # Paths with status - output = self._bzr(['status', '--short', '--no-classify']).rstrip('\n') + output = self._run(['status', '--short', '--no-classify']).rstrip('\n') for line in output.split('\n'): - statuses[os.path.normpath(line[4:])] = self._bzr_status_translate(line[:2]) + statuses[os.path.normpath(line[4:])] = self._status_translate(line[:2]) return statuses @@ -125,7 +121,7 @@ class Bzr(Vcs): def data_branch(self): try: - return self._bzr(['nick']).rstrip('\n') or None + return self._run(['nick']).rstrip('\n') or None except VcsError: return None diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py index ca49998e..8f4d9ff8 100644 --- a/ranger/ext/vcs/git.py +++ b/ranger/ext/vcs/git.py @@ -28,19 +28,15 @@ class Git(Vcs): # Generic - def _git(self, args, path=None, catchout=True, retbytes=False): - """Run a git command""" - return self._vcs(['git'] + args, path or self.path, catchout=catchout, retbytes=retbytes) - def _head_ref(self): """Returns HEAD reference""" - return self._git(['symbolic-ref', self.HEAD]).rstrip('\n') or None + return self._run(['symbolic-ref', self.HEAD]).rstrip('\n') or None def _remote_ref(self, ref): """Returns remote reference associated to given ref""" if ref is None: return None - return self._git(['for-each-ref', '--format=%(upstream)', ref]).rstrip('\n') or None + return self._run(['for-each-ref', '--format=%(upstream)', ref]).rstrip('\n') or None def _log(self, refspec=None, maxres=None, filelist=None): """Returns an array of dicts containing revision info for refspec""" @@ -62,7 +58,7 @@ class Git(Vcs): args += ['--'] + filelist try: - output = self._git(args).rstrip('\n') + output = self._run(args).rstrip('\n') except VcsError: return None if not output: @@ -89,13 +85,13 @@ class Git(Vcs): args = ['add', '--all'] if filelist: args += ['--'] + filelist - self._git(args, catchout=False) + self._run(args, catchout=False) def action_reset(self, filelist=None): args = ['reset'] if filelist: args += ['--'] + filelist - self._git(args, catchout=False) + self._run(args, catchout=False) # Data Interface @@ -104,7 +100,7 @@ class Git(Vcs): # Paths with status skip = False - output = self._git(['status', '--porcelain', '-z']).rstrip('\x00') + output = self._run(['status', '--porcelain', '-z']).rstrip('\x00') if not output: return 'sync' for line in output.split('\x00'): @@ -124,7 +120,7 @@ class Git(Vcs): statuses = {} # Ignored directories - output = self._git([ + output = self._run([ 'ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard' ]).rstrip('\x00') if output: @@ -133,7 +129,7 @@ class Git(Vcs): statuses[os.path.normpath(path)] = 'ignored' # Empty directories - output = self._git( + output = self._run( ['ls-files', '-z', '--others', '--directory', '--exclude-standard']).rstrip('\x00') if output: for path in output.split('\x00'): @@ -141,7 +137,7 @@ class Git(Vcs): statuses[os.path.normpath(path)] = 'none' # Paths with status - output = self._git(['status', '--porcelain', '-z', '--ignored']).rstrip('\x00') + output = self._run(['status', '--porcelain', '-z', '--ignored']).rstrip('\x00') if output: skip = False for line in output.split('\x00'): @@ -163,7 +159,7 @@ class Git(Vcs): if not head or not remote: return 'none' - output = self._git(['rev-list', '--left-right', '{0:s}...{1:s}'.format(remote, head)]) + output = self._run(['rev-list', '--left-right', '{0:s}...{1:s}'.format(remote, head)]) ahead = re.search(r'^>', output, flags=re.MULTILINE) behind = re.search(r'^<', output, flags=re.MULTILINE) if ahead: diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index f1e9dbdb..2d047de3 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -25,10 +25,6 @@ class Hg(Vcs): # Generic - def _hg(self, args, path=None, catchout=True, retbytes=False): - """Run a hg command""" - return self._vcs(['hg'] + args, path or self.path, catchout=catchout, retbytes=retbytes) - def _log(self, refspec=None, maxres=None, filelist=None): args = [ @@ -49,7 +45,7 @@ class Hg(Vcs): args += ['--'] + filelist try: - output = self._hg(args).rstrip('\n') + output = self._run(args).rstrip('\n') except VcsError: return None if not output: @@ -66,7 +62,7 @@ class Hg(Vcs): def _remote_url(self): """Remote url""" try: - return self._hg(['showconfig', 'paths.default']).rstrip('\n') or None + return self._run(['showconfig', 'paths.default']).rstrip('\n') or None except VcsError: return None @@ -83,7 +79,7 @@ class Hg(Vcs): args = ['add'] if filelist: args += ['--'] + filelist - self._hg(args, catchout=False) + self._run(args, catchout=False) def action_reset(self, filelist=None): args = ['forget', '--'] @@ -91,7 +87,7 @@ class Hg(Vcs): args += filelist else: args += self.rootvcs.status_subpaths.keys() - self._hg(args, catchout=False) + self._run(args, catchout=False) # Data interface @@ -99,7 +95,7 @@ class Hg(Vcs): statuses = set() # Paths with status - output = self._hg(['status', '--all', '--print0']).rstrip('\x00') + output = self._run(['status', '--all', '--print0']).rstrip('\x00') if not output: return 'sync' for line in output.split('\x00'): @@ -117,7 +113,7 @@ class Hg(Vcs): statuses = {} # Paths with status - output = self._hg(['status', '--all', '--print0']).rstrip('\x00') + output = self._run(['status', '--all', '--print0']).rstrip('\x00') if output: for line in output.split('\x00'): code, path = line[0], line[2:] @@ -133,12 +129,12 @@ class Hg(Vcs): ahead = behind = True try: - self._hg(['outgoing'], catchout=False) + self._run(['outgoing'], catchout=False) except VcsError: ahead = False try: - self._hg(['incoming'], catchout=False) + self._run(['incoming'], catchout=False) except VcsError: behind = False @@ -148,7 +144,7 @@ class Hg(Vcs): return 'behind' if behind else 'sync' def data_branch(self): - return self._hg(['branch'], catchout=True).rstrip('\n') or None + return self._run(['branch'], catchout=True).rstrip('\n') or None def data_info(self, rev=None): if rev is None: diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index a99f9aa9..4cf38a97 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -115,8 +115,12 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes # Generic - def _vcs(self, cmd, path, catchout=True, retbytes=False): # pylint: disable=no-self-use - """Run a VCS command""" + def _run(self, args, path=None, catchout=True, retbytes=False): + """Run a command""" + cmd = [self.repotype] + args + if path is None: + path = self.path + with open(os.devnull, 'w') as devnull: try: if catchout: -- cgit 1.4.1-2-gfad0 From 61a6dcf23a9bb8e78b7807a5099b7e046f9fa0c7 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 04:39:34 +0100 Subject: VCS: Implement Subversion --- ranger/ext/vcs/hg.py | 2 +- ranger/ext/vcs/svn.py | 212 +++++++++++++++++++++++++------------------------- 2 files changed, 109 insertions(+), 105 deletions(-) diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index 2d047de3..c4b04282 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -144,7 +144,7 @@ class Hg(Vcs): return 'behind' if behind else 'sync' def data_branch(self): - return self._run(['branch'], catchout=True).rstrip('\n') or None + return self._run(['branch']).rstrip('\n') or None def data_info(self, rev=None): if rev is None: diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index 14e82db0..dd4270d8 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -3,142 +3,146 @@ """Subversion module""" -from __future__ import with_statement - from datetime import datetime -import logging import os -import re +from xml.etree import ElementTree as etree from .vcs import Vcs, VcsError + class SVN(Vcs): """VCS implementation for Subversion""" - HEAD = 'HEAD' - # Generic - #--------------------------- + _status_translations = ( + ('ADR', 'staged'), + ('C', 'conflict'), + ('I', 'ignored'), + ('M~', 'changed'), + ('X', 'none'), + ('?', 'untracked'), + ('!', 'deleted'), + ) - def _svn(self, path, args, silent=True, catchout=False, retbytes=False): - return self._vcs(path, 'svn', args, silent=silent, catchout=catchout, retbytes=retbytes) + def _log(self, refspec=None, maxres=None, filelist=None): + """ Retrieves log message and parses it""" + args = ['log', '--xml'] - def _has_head(self): - """Checks whether repo has head""" - return True + if refspec: + args += ['--limit', '1', '--revision', refspec] + elif maxres: + args += ['--limit', str(maxres)] - def _sanitize_rev(self, rev): - if rev == None: return None - rev = rev.strip() - if len(rev) == 0: return None - if rev[-1] == '+': rev = rev[:-1] + if filelist: + args += ['--'] + filelist try: - if int(rev) == 0: return None - except: - pass - - return rev - - def _log(self, refspec=None, maxres=None, filelist=None): - """ Retrieves log message and parses it. - """ - args = ['log'] - - if refspec: args = args + ['--limit', '1', '-r', refspec] - elif maxres: args = args + ['--limit', str(maxres)] - - if filelist: args = args + filelist - logging.debug('Running svn log') - logging.debug(args) - - raw = self._svn(self.path, args, catchout=True) - logging.debug(raw) - L = re.findall(r"""^[-]*\s* # Dash line - r([0-9]*)\s\|\s # Revision [0] - (.*)\s\|\s # Author [1] - (.*?)\s # Date [2] - (.*?)\s # Time [3] - .*?\|\s*? # Dump rest of date string - ([0-9])+\sline(?:s)?\s* # Number of line(s) [4] - (.*)\s # Log Message [5] - [-]+\s* # Dash line - $""", raw, re.MULTILINE | re.VERBOSE) + output = self._run(args).rstrip('\n') + except VcsError: + return None + if not output: + return None log = [] - for t in L: - logging.debug(t) - dt = {} - dt['short'] = t[0].strip() - dt['revid'] = t[0].strip() - dt['author'] = t[1].strip() - dt['date'] = datetime.strptime(t[2]+'T'+t[3], "%Y-%m-%dT%H:%M:%S") - dt['summary'] = t[5].strip() - log.append(dt) - logging.debug(log) + for entry in etree.fromstring(output).findall('./logentry'): + new = {} + new['short'] = entry.get('revision') + new['revid'] = entry.get('revision') + new['author'] = entry.find('./author').text + new['date'] = datetime.strptime( + entry.find('./date').text, + '%Y-%m-%dT%H:%M:%S.%fZ', + ) + new['summary'] = entry.find('./msg').text.split('\n')[0] + log.append(new) return log - def _svn_file_status(self, st): - if len(st) != 1: raise VcsError("Wrong hg file status string: %s" % st) - if st in "A": return 'staged' - elif st in "D": return 'deleted' - elif st in "I": return 'ignored' - elif st in "?": return 'untracked' - elif st in "C": return 'conflict' - else: return 'unknown' + def _status_translate(self, code): + """Translate status code""" + for code_x, status in self._status_translations: + if code in code_x: + return status + return 'unknown' + + def _remote_url(self): + """Remote url""" + try: + output = self._run(['info', '--xml']).rstrip('\n') + except VcsError: + return None + if not output: + return None + return etree.fromstring(output).find('./entry/url').text or None # Action Interface - #--------------------------- def action_add(self, filelist=None): - """Adds files to the index, preparing for commit""" - if filelist != None: self._svn(self.path, ['add'] + filelist) - else: self._svn(self.path, ['add']) + args = ['add'] + if filelist: + args += ['--'] + filelist + self._run(args, catchout=False) def action_reset(self, filelist=None): - """Equivalent to svn revert""" - if filelist == None: filelist = self.data_status_subpaths().keys() - self._svn(self.path, ['revert'] + filelist) + args = ['revert', '--'] + if filelist: + args += filelist + else: + args += self.rootvcs.status_subpaths.keys() + self._run(args, catchout=False) # Data Interface - #--------------------------- + + def data_status_root(self): + statuses = set() + + # Paths with status + output = self._run(['status']).rstrip('\n') + if not output: + return 'sync' + for line in output.split('\n'): + code = line[0] + if code == ' ': + continue + statuses.add(self._status_translate(code)) + + for status in self.DIRSTATUSES: + if status in statuses: + return status + return 'sync' def data_status_subpaths(self): - """Returns a dict indexed by files not in sync their status as values. - Paths are given relative to the root. Strips trailing '/' from dirs.""" - raw = self._svn(self.path, ['status'], catchout=True, retbytes=True) -# logging.debug(raw) - L = re.findall(r'^(.)\s*(.*?)\s*$', raw.decode('utf-8'), re.MULTILINE) - ret = {} - for st, p in L: - # Detect conflict by the existence of .orig files - if st == '?' and re.match(r'^.*\.orig\s*$', p): st = 'X' - sta = self._svn_file_status(st) - ret[os.path.normpath(p.strip())] = sta - return ret + statuses = {} - def data_status_remote(self): - """Checks the status of the repo regarding sync state with remote branch. + # Paths with status + output = self._run(['status']).rstrip('\n') + if output: + for line in output.split('\n'): + code, path = line[0], line[8:] + if code == ' ': + continue + statuses[os.path.normpath(path)] = self._status_translate(code) - I'm not sure this make sense for SVN so we're just going to return 'sync'""" - return 'sync' + return statuses + + def data_status_remote(self): + remote = self._remote_url() + if remote is None or remote.startswith('file://'): + return 'none' + return 'unknown' def data_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" return None - branch = self._svn(self.path, ['branch'], catchout=True) - return branch or None def data_info(self, rev=None): - """Gets info about the given revision rev""" - if rev == None: rev = self.HEAD - rev = self._sanitize_rev(rev) - if rev == self.HEAD and not self._has_head(): return None - logging.debug('refspec is ' + str(rev)) - L = self._log(refspec=rev) - logging.debug(len(L)) - 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)) -- cgit 1.4.1-2-gfad0 From a366c4ae69dc8b6f5958468c8c0ba0c58d85312a Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 05:00:56 +0100 Subject: VCS: Disable background updating due to lock issues --- ranger/ext/vcs/vcs.py | 66 ++++++++------------------------------------------- ranger/gui/ui.py | 2 +- 2 files changed, 11 insertions(+), 57 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 4cf38a97..b5e40fec 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -48,10 +48,10 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes # Backends REPOTYPES = { - 'bzr': {'class': 'Bzr', 'setting': 'vcs_backend_bzr', 'lazy': True}, - 'git': {'class': 'Git', 'setting': 'vcs_backend_git', 'lazy': False}, - 'hg': {'class': 'Hg', 'setting': 'vcs_backend_hg', 'lazy': True}, - 'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn', 'lazy': True}, + 'bzr': {'class': 'Bzr', 'setting': 'vcs_backend_bzr'}, + 'git': {'class': 'Git', 'setting': 'vcs_backend_git'}, + 'hg': {'class': 'Hg', 'setting': 'vcs_backend_hg'}, + 'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn'}, } # Possible directory statuses in order of importance @@ -361,14 +361,12 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attributes """VCS thread""" - def __init__(self, ui, idle_delay): + def __init__(self, ui): super(VcsThread, self).__init__() self.daemon = True self.ui = ui # pylint: disable=invalid-name - self.delay = idle_delay self.queue = queue.Queue() self.wake = threading.Event() - self.awoken = False self.timestamp = time.time() self.redraw = False self.roots = set() @@ -380,18 +378,6 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut return True return False - def _targeted_directory_rightmost(self): - """Return rightmost targeted directory""" - target = self.ui.browser.columns[-1].target - if target: - if target.is_directory: - return target - else: - target = self.ui.browser.columns[-2].target - if target and target.is_directory: - return target - return None - def _queue_process(self): # pylint: disable=too-many-branches """Process queue: Initialize roots under dirobj""" @@ -436,44 +422,12 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut self.redraw = True dirobj.has_vcschild = has_vcschild - def _update_columns(self): - """Update targeted directories""" - for column in self.ui.browser.columns: - target = column.target - if target and target.is_directory and target.vcs: - # Redraw if tree is purged - if not target.vcs.check(): - self.redraw = True - - if target.vcs.track and target.vcs.root not in self.roots: - self.roots.add(target.vcs.root) - lazy = target.vcs.REPOTYPES[target.vcs.repotype]['lazy'] - if ((lazy and target.vcs.rootvcs.check_outdated()) or not lazy) \ - and target.vcs.rootvcs.update_root(): - target.vcs.rootvcs.update_tree() - self.redraw = True - def run(self): while True: - curtime = time.time() - if self.wake.wait(timeout=((self.timestamp + self.delay) - curtime)): - self.awoken = True + if self.wake.wait(): self.wake.clear() - if self._hindered(): - continue - - if self.awoken: - self._queue_process() - else: - self.timestamp = curtime - - # Exclude root if repodir in the rightmost column (causes strobing) - target = self._targeted_directory_rightmost() - if target and target.vcs and target.vcs.in_repodir: - self.roots.add(target.vcs.root) - - self._update_columns() + self._queue_process() if self.redraw: self.redraw = False @@ -481,11 +435,11 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut if column.target and column.target.is_directory: column.need_redraw = True self.ui.status.need_redraw = True - if self.awoken: - self.ui.redraw() + while self._hindered(): + time.sleep(0.01) + self.ui.redraw() self.roots.clear() - self.awoken = False def wakeup(self, dirobj): """Wakeup thread""" diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index c3f68586..ff3ced57 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -253,7 +253,7 @@ class UI(DisplayableContainer): self.add_child(self.pager) # Create VCS thread - self.vcsthread = VcsThread(self, max(1, self.settings.idle_delay / 1000)) + self.vcsthread = VcsThread(self) self.vcsthread.start() def redraw(self): -- cgit 1.4.1-2-gfad0 From f38066d3bf60659c2cd45e7ba1ea4b7434e653cd Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 13:40:09 +0100 Subject: VCS: hg: Use json template --- ranger/ext/vcs/hg.py | 52 ++++++++++++++++++++-------------------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index c4b04282..fa74bfb5 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -27,16 +27,7 @@ class Hg(Vcs): def _log(self, refspec=None, maxres=None, filelist=None): - args = [ - 'log', '--template', - '\\{' - '\\x00short\\x00:\\x00{rev}\\x00,' - '\\x00revid\\x00:\\x00{node}\\x00,' - '\\x00author\\x00:\\x00{author}\\x00,' - '\\x00date\\x00:\\x00{date}\\x00,' - '\\x00summary\\x00:\\x00{desc}\\x00' - '}\\n' - ] + args = ['log', '--template', 'json'] if refspec: args += ['--limit', '1', '--rev', refspec] elif maxres: @@ -52,11 +43,14 @@ class Hg(Vcs): return None log = [] - for line in output\ - .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').split('\n'): - line = json.loads(line) - line['date'] = datetime.fromtimestamp(float(line['date'].split('-')[0])) - log.append(line) + for entry in json.loads(output): + new = {} + new['short'] = entry['rev'] + new['revid'] = entry['node'] + new['author'] = entry['user'] + new['date'] = datetime.fromtimestamp(entry['date'][0]) + new['summary'] = entry['desc'] + log.append(new) return log def _remote_url(self): @@ -95,31 +89,25 @@ class Hg(Vcs): statuses = set() # Paths with status - output = self._run(['status', '--all', '--print0']).rstrip('\x00') - if not output: - return 'sync' - for line in output.split('\x00'): - code = line[0] - if code == 'C': + for entry in json.loads(self._run(['status', '--all', '--template', 'json'])): + if entry['status'] == 'C': continue - statuses.add(self._status_translate(code)) + statuses.add(self._status_translate(entry['status'])) - for status in self.DIRSTATUSES: - if status in statuses: - return status + if statuses: + for status in self.DIRSTATUSES: + if status in statuses: + return status return 'sync' def data_status_subpaths(self): statuses = {} # Paths with status - output = self._run(['status', '--all', '--print0']).rstrip('\x00') - if output: - for line in output.split('\x00'): - code, path = line[0], line[2:] - if code == 'C': - continue - statuses[os.path.normpath(path)] = self._status_translate(code) + for entry in json.loads(self._run(['status', '--all', '--template', 'json'])): + if entry['status'] == 'C': + continue + statuses[os.path.normpath(entry['path'])] = self._status_translate(entry['status']) return statuses -- cgit 1.4.1-2-gfad0 From afd2850957a21d68fc2843eba5e4b45263e08a0d Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 14:52:59 +0100 Subject: VCS: Fix root updating --- ranger/ext/vcs/vcs.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index b5e40fec..816fad3f 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -366,10 +366,9 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut self.daemon = True self.ui = ui # pylint: disable=invalid-name self.queue = queue.Queue() - self.wake = threading.Event() + self.awoken = threading.Event() self.timestamp = time.time() self.redraw = False - self.roots = set() def _hindered(self): """Check for hinders""" @@ -380,18 +379,19 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut def _queue_process(self): # pylint: disable=too-many-branches """Process queue: Initialize roots under dirobj""" - + roots = set() while True: try: dirobj = self.queue.get(block=False) except queue.Empty: break - # Update if root - if dirobj.vcs.track and dirobj.vcs.is_root: - self.roots.add(dirobj.vcs.path) - if dirobj.vcs.update_root(): - dirobj.vcs.update_tree() + if dirobj.vcs.track: + if dirobj.vcs.rootvcs.path not in roots \ + and dirobj.vcs.rootvcs.check_outdated() \ + and dirobj.vcs.rootvcs.update_root(): + roots.add(dirobj.vcs.rootvcs.path) + dirobj.vcs.rootvcs.update_tree() self.redraw = True if dirobj.files_all is None: @@ -424,8 +424,8 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut def run(self): while True: - if self.wake.wait(): - self.wake.clear() + self.awoken.wait() + self.awoken.clear() self._queue_process() @@ -439,12 +439,10 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut time.sleep(0.01) self.ui.redraw() - self.roots.clear() - def wakeup(self, dirobj): """Wakeup thread""" self.queue.put(dirobj) - self.wake.set() + self.awoken.set() # Backend imports import ranger.ext.vcs.bzr # NOQA pylint: disable=wrong-import-position -- cgit 1.4.1-2-gfad0 From 878f0f574ca96c6e19c86765159d6ce40084f741 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 15:21:51 +0100 Subject: VCS: hg: Disable remote status due to performance issues --- ranger/ext/vcs/hg.py | 17 +---------------- ranger/ext/vcs/vcs.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py index fa74bfb5..cf15e35e 100644 --- a/ranger/ext/vcs/hg.py +++ b/ranger/ext/vcs/hg.py @@ -114,22 +114,7 @@ class Hg(Vcs): def data_status_remote(self): if self._remote_url() is None: return 'none' - - ahead = behind = True - try: - self._run(['outgoing'], catchout=False) - except VcsError: - ahead = False - - try: - self._run(['incoming'], catchout=False) - except VcsError: - behind = False - - if ahead: - return 'diverged' if behind else 'ahead' - else: - return 'behind' if behind else 'sync' + return 'unknown' def data_branch(self): return self._run(['branch']).rstrip('\n') or None diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 816fad3f..a0f4b5d7 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -160,6 +160,17 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes return (None, None, None, None) + def _status_root(self): + """Returns root status""" + if self.status_subpaths is None: + return 'none' + + statuses = set(status for path, status in self.status_subpaths.items()) + for status in self.DIRSTATUSES: + if status in statuses: + return status + return 'sync' + def init_root(self): """Initialize root cheaply""" try: @@ -245,7 +256,7 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes self.branch = self.data_branch() self.status_subpaths = self.data_status_subpaths() self.obj.vcsremotestatus = self.data_status_remote() - self.obj.vcsstatus = self.status_root() + self.obj.vcsstatus = self._status_root() except VcsError: self.update_tree(purge=True) return False @@ -284,17 +295,6 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes return True return False - def status_root(self): - """Returns root status""" - if self.status_subpaths is None: - return 'none' - - statuses = set(status for path, status in self.status_subpaths.items()) - for status in self.DIRSTATUSES: - if status in statuses: - return status - return 'sync' - def status_subpath(self, path, is_directory=False): """ Returns the status of path -- cgit 1.4.1-2-gfad0 From 6edb035a4d176c4df852b7e7ad9be89a3de24a72 Mon Sep 17 00:00:00 2001 From: nfnty Date: Mon, 8 Feb 2016 16:32:46 +0100 Subject: VCS: Split Vcs root into VcsRoot --- ranger/ext/vcs/vcs.py | 145 +++++++++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 61 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index a0f4b5d7..5c456120 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -78,21 +78,14 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes self.root, self.repodir, self.repotype, self.links = self._find_root(self.path) self.is_root = True if self.obj.path == self.root else False - + self.in_repodir = False self.rootvcs = None - self.rootinit = False - self.head = None - self.branch = None - self.updatetime = None self.track = False - self.in_repodir = False - self.status_subpaths = None if self.root: if self.is_root: self.rootvcs = self - self.__class__ = getattr(getattr(ranger.ext.vcs, self.repotype), - self.REPOTYPES[self.repotype]['class']) + self.__class__ = globals()[self.REPOTYPES[self.repotype]['class'] + 'Root'] if not os.access(self.repodir, os.R_OK): self.obj.vcsremotestatus = 'unknown' @@ -106,7 +99,7 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes if self.rootvcs.root is None: return self.rootvcs.links |= self.links - self.__class__ = self.rootvcs.__class__ + self.__class__ = globals()[self.REPOTYPES[self.repotype]['class']] self.track = self.rootvcs.track if self.path == self.repodir or self.path.startswith(self.repodir + '/'): @@ -160,6 +153,62 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes return (None, None, None, None) + def check(self): + """Check health""" + if not self.in_repodir \ + and (not self.track or (not self.is_root and self._get_repotype(self.path)[0])): + self.__init__(self.obj) + return False + elif self.track and not os.path.exists(self.repodir): + self.rootvcs.update_tree(purge=True) # pylint: disable=no-member + return False + return True + + # Action interface + + def action_add(self, filelist): + """Adds files to the index""" + raise NotImplementedError + + def action_reset(self, filelist): + """Removes files from the index""" + raise NotImplementedError + + # Data interface + + def data_status_root(self): + """Returns status of self.root cheaply""" + raise NotImplementedError + + def data_status_subpaths(self): + """Returns a dict indexed by subpaths not in sync with their status as values. + Paths are given relative to self.root""" + raise NotImplementedError + + def data_status_remote(self): + """ + Returns remote status of repository + One of ('sync', 'ahead', 'behind', 'diverged', 'none') + """ + raise NotImplementedError + + def data_branch(self): + """Returns the current named branch, if this makes sense for the backend. None otherwise""" + raise NotImplementedError + + def data_info(self, rev=None): + """Returns info string about revision rev. None in special cases""" + raise NotImplementedError + + +class VcsRoot(Vcs): # pylint: disable=abstract-method + """Vcs root""" + rootinit = False + head = None + branch = None + updatetime = None + status_subpaths = None + def _status_root(self): """Returns root status""" if self.status_subpaths is None: @@ -264,17 +313,6 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes self.updatetime = time.time() return True - def check(self): - """Check health""" - if not self.in_repodir \ - and (not self.track or (not self.is_root and self._get_repotype(self.path)[0])): - self.__init__(self.obj) - return False - elif self.track and not os.path.exists(self.repodir): - self.rootvcs.update_tree(purge=True) - return False - return True - def check_outdated(self): """Check if root is outdated""" if self.updatetime is None: @@ -322,42 +360,6 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes return status return 'sync' - # Action interface - - def action_add(self, filelist): - """Adds files to the index""" - raise NotImplementedError - - def action_reset(self, filelist): - """Removes files from the index""" - raise NotImplementedError - - # Data interface - - def data_status_root(self): - """Returns status of self.root cheaply""" - raise NotImplementedError - - def data_status_subpaths(self): - """Returns a dict indexed by subpaths not in sync with their status as values. - Paths are given relative to self.root""" - raise NotImplementedError - - def data_status_remote(self): - """ - Returns remote status of repository - One of ('sync', 'ahead', 'behind', 'diverged', 'none') - """ - raise NotImplementedError - - def data_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" - raise NotImplementedError - - def data_info(self, rev=None): - """Returns info string about revision rev. None in special cases""" - raise NotImplementedError - class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attributes """VCS thread""" @@ -444,8 +446,29 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut self.queue.put(dirobj) self.awoken.set() + # Backend imports -import ranger.ext.vcs.bzr # NOQA pylint: disable=wrong-import-position -import ranger.ext.vcs.git # NOQA pylint: disable=wrong-import-position -import ranger.ext.vcs.hg # NOQA pylint: disable=wrong-import-position -import ranger.ext.vcs.svn # NOQA pylint: disable=wrong-import-position +from .bzr import Bzr # NOQA pylint: disable=wrong-import-position +from .git import Git # NOQA pylint: disable=wrong-import-position +from .hg import Hg # NOQA pylint: disable=wrong-import-position +from .svn import SVN # NOQA pylint: disable=wrong-import-position + + +class BzrRoot(VcsRoot, Bzr): + ''' Bzr root ''' + pass + + +class GitRoot(VcsRoot, Git): + ''' Git root ''' + pass + + +class HgRoot(VcsRoot, Hg): + ''' Hg root ''' + pass + + +class SVNRoot(VcsRoot, SVN): + ''' SVN root ''' + pass -- cgit 1.4.1-2-gfad0 From de592fa35bb7fed0de47f3294566b31c470dbb6a Mon Sep 17 00:00:00 2001 From: nfnty Date: Tue, 9 Feb 2016 14:51:05 +0100 Subject: VCS: Process queue more efficiently --- ranger/container/directory.py | 3 +- ranger/ext/vcs/vcs.py | 91 ++++++++++++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index cd814cb1..2ee250ea 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -333,7 +333,8 @@ class Directory(FileSystemObject, Accumulator, Loadable): item.relative_path = item.basename item.relative_path_lower = item.relative_path.lower() if item.vcs and item.vcs.track and not item.vcs.is_root: - item.vcsstatus = item.vcs.rootvcs.status_subpath(item.path) + item.vcsstatus = item.vcs.rootvcs.status_subpath( + item.path, is_directory=True) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 5c456120..4710feb6 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -371,6 +371,7 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut self.awoken = threading.Event() self.timestamp = time.time() self.redraw = False + self.roots = set() def _hindered(self): """Check for hinders""" @@ -379,50 +380,78 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut return True return False + def _is_targeted(self, dirobj): + ''' Check if dirobj is targeted ''' + if not self.ui.browser.main_column: + return False + target = self.ui.browser.main_column.target + if target and target.is_directory and target.path == dirobj.path: + return True + return False + + def _update_files(self, fileobjs): + ''' Update files ''' + if not fileobjs: + return False + + has_vcschild = False + for fileobj in fileobjs: + if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: + continue + + if fileobj.vcs.is_root: + has_vcschild = True + if not fileobj.vcs.rootinit and not self._is_targeted(fileobj): + fileobj.vcs.init_root() + self.roots.add(fileobj.vcs.path) + self.redraw = True + + elif fileobj.is_link: + rootvcs = fileobj.vcs.rootvcs + realpath = os.path.realpath(fileobj.path) + if realpath == fileobj.vcs.root: + has_vcschild = True + if not rootvcs.rootinit and not self._is_targeted(rootvcs.obj): + rootvcs.init_root() + self.roots.add(rootvcs.path) + fileobj.vcsstatus = rootvcs.obj.vcsstatus + fileobj.vcsremotestatus = rootvcs.obj.vcsremotestatus + else: + fileobj.vcsstatus = rootvcs.status_subpath(realpath) + self.redraw = True + + return has_vcschild + def _queue_process(self): # pylint: disable=too-many-branches """Process queue: Initialize roots under dirobj""" - roots = set() + dirobjs = [] + paths = set() + self.roots.clear() + while True: try: - dirobj = self.queue.get(block=False) + dirobjs.append(self.queue.get(block=False)) except queue.Empty: break - if dirobj.vcs.track: - if dirobj.vcs.rootvcs.path not in roots \ - and dirobj.vcs.rootvcs.check_outdated() \ - and dirobj.vcs.rootvcs.update_root(): - roots.add(dirobj.vcs.rootvcs.path) - dirobj.vcs.rootvcs.update_tree() - self.redraw = True - - if dirobj.files_all is None: + for dirobj in dirobjs: + if dirobj.path in paths: continue + paths.add(dirobj.path) - has_vcschild = False - for fileobj in dirobj.files_all: - if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: - continue - if fileobj.vcs.is_root: - has_vcschild = True - if not fileobj.vcs.rootinit: - fileobj.vcs.init_root() - self.redraw = True - elif fileobj.is_link: - if os.path.realpath(fileobj.path) == fileobj.vcs.root: - has_vcschild = True - if not fileobj.vcs.rootvcs.rootinit: - fileobj.vcs.rootvcs.init_root() - fileobj.vcsstatus = fileobj.vcs.rootvcs.obj.vcsstatus - fileobj.vcsremotestatus = fileobj.vcs.rootvcs.obj.vcsremotestatus - else: - fileobj.vcsstatus = fileobj.vcs.rootvcs.status_subpath( - os.path.realpath(fileobj.path)) + if dirobj.vcs.track: + rootvcs = dirobj.vcs.rootvcs + if rootvcs.path not in self.roots \ + and rootvcs.check_outdated() and rootvcs.update_root(): + rootvcs.update_tree() + self.roots.add(rootvcs.path) self.redraw = True + has_vcschild = self._update_files(dirobj.files_all) + if dirobj.has_vcschild != has_vcschild: - self.redraw = True dirobj.has_vcschild = has_vcschild + self.redraw = True def run(self): while True: -- cgit 1.4.1-2-gfad0 From 09d61c80dafc52bdb274a4ae1ad3a99a61a0042f Mon Sep 17 00:00:00 2001 From: nfnty Date: Tue, 9 Feb 2016 17:38:04 +0100 Subject: VCS: Fix link handling --- ranger/container/directory.py | 7 +++-- ranger/ext/vcs/vcs.py | 64 ++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index 2ee250ea..fdaf1ee8 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -332,16 +332,17 @@ class Directory(FileSystemObject, Accumulator, Loadable): else: item.relative_path = item.basename item.relative_path_lower = item.relative_path.lower() - if item.vcs and item.vcs.track and not item.vcs.is_root: + if item.vcs and item.vcs.track and not item.vcs.is_root_pointer: item.vcsstatus = item.vcs.rootvcs.status_subpath( - item.path, is_directory=True) + os.path.join(self.realpath, item.basename), is_directory=True) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) item.load() disk_usage += item.size if self.vcs and self.vcs.track: - item.vcsstatus = self.vcs.rootvcs.status_subpath(item.path) + item.vcsstatus = self.vcs.rootvcs.status_subpath( + os.path.join(self.realpath, item.basename)) files.append(item) self.percent = 100 * len(files) // len(filenames) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index 4710feb6..e7f19f16 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -78,6 +78,8 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes self.root, self.repodir, self.repotype, self.links = self._find_root(self.path) self.is_root = True if self.obj.path == self.root else False + self.is_root_link = True if self.obj.is_link and self.obj.realpath == self.root else False + self.is_root_pointer = self.is_root or self.is_root_link self.in_repodir = False self.rootvcs = None self.track = False @@ -242,6 +244,10 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method except KeyError: wdirs[:] = [] continue + if not wrootobj.vcs.track: + wdirs[:] = [] + continue + if wrootobj.content_loaded: has_vcschild = False for fileobj in wrootobj.files_all: @@ -257,23 +263,26 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method fileobj.vcs.check() if not fileobj.vcs.track: continue - if fileobj.vcs.is_root: + if fileobj.vcs.is_root_pointer: has_vcschild = True else: fileobj.vcsstatus = self.status_subpath( - fileobj.path, is_directory=True) + os.path.join(wrootobj.realpath, fileobj.basename), + is_directory=True, + ) else: - fileobj.vcsstatus = self.status_subpath(fileobj.path) + fileobj.vcsstatus = self.status_subpath( + os.path.join(wrootobj.realpath, fileobj.basename)) wrootobj.has_vcschild = has_vcschild # Remove dead directories for wdir in list(wdirs): try: - wdir_obj = self.obj.fm.directories[os.path.join(wroot, wdir)] + wdirobj = self.obj.fm.directories[os.path.join(wroot, wdir)] except KeyError: wdirs.remove(wdir) continue - if wdir_obj.vcs.is_root or not wdir_obj.vcs.track: + if not wdirobj.vcs.track or wdirobj.vcs.is_root_pointer: wdirs.remove(wdir) def update_tree(self, purge=False): @@ -292,9 +301,6 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method elif dirobj.vcs.path == self.path: dirobj.vcsremotestatus = self.obj.vcsremotestatus dirobj.vcsstatus = self.obj.vcsstatus - else: - dirobj.vcsstatus = self.status_subpath( - os.path.realpath(dirobj.path), is_directory=True) if purge: self.__init__(self.obj) @@ -321,7 +327,7 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method 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: + if wroot != self.path and wrootobj.vcs.is_root_pointer: wdirs[:] = [] continue @@ -382,14 +388,11 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut def _is_targeted(self, dirobj): ''' Check if dirobj is targeted ''' - if not self.ui.browser.main_column: - return False - target = self.ui.browser.main_column.target - if target and target.is_directory and target.path == dirobj.path: + if self.ui.browser.main_column and self.ui.browser.main_column.target == dirobj: return True return False - def _update_files(self, fileobjs): + def _update_subroots(self, fileobjs): ''' Update files ''' if not fileobjs: return False @@ -399,26 +402,17 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: continue - if fileobj.vcs.is_root: + rootvcs = fileobj.vcs.rootvcs + if fileobj.vcs.is_root_pointer: has_vcschild = True - if not fileobj.vcs.rootinit and not self._is_targeted(fileobj): - fileobj.vcs.init_root() - self.roots.add(fileobj.vcs.path) + if not rootvcs.rootinit and not self._is_targeted(rootvcs.obj): + self.roots.add(rootvcs.path) + rootvcs.init_root() self.redraw = True - - elif fileobj.is_link: - rootvcs = fileobj.vcs.rootvcs - realpath = os.path.realpath(fileobj.path) - if realpath == fileobj.vcs.root: - has_vcschild = True - if not rootvcs.rootinit and not self._is_targeted(rootvcs.obj): - rootvcs.init_root() - self.roots.add(rootvcs.path) + if fileobj.is_link: fileobj.vcsstatus = rootvcs.obj.vcsstatus fileobj.vcsremotestatus = rootvcs.obj.vcsremotestatus - else: - fileobj.vcsstatus = rootvcs.status_subpath(realpath) - self.redraw = True + self.redraw = True return has_vcschild @@ -441,13 +435,13 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut if dirobj.vcs.track: rootvcs = dirobj.vcs.rootvcs - if rootvcs.path not in self.roots \ - and rootvcs.check_outdated() and rootvcs.update_root(): - rootvcs.update_tree() + if rootvcs.path not in self.roots and rootvcs.check_outdated(): self.roots.add(rootvcs.path) - self.redraw = True + if rootvcs.update_root(): + rootvcs.update_tree() + self.redraw = True - has_vcschild = self._update_files(dirobj.files_all) + has_vcschild = self._update_subroots(dirobj.files_all) if dirobj.has_vcschild != has_vcschild: dirobj.has_vcschild = has_vcschild -- cgit 1.4.1-2-gfad0 From 44228d38c781e0d65209bb00e9ff04ea925d0343 Mon Sep 17 00:00:00 2001 From: nfnty Date: Tue, 9 Feb 2016 19:21:39 +0100 Subject: VCS: Rename fileobj to fsobj, remove _hindered --- ranger/ext/vcs/svn.py | 2 +- ranger/ext/vcs/vcs.py | 73 +++++++++++++++++++++++---------------------------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py index dd4270d8..1813f857 100644 --- a/ranger/ext/vcs/svn.py +++ b/ranger/ext/vcs/svn.py @@ -24,7 +24,7 @@ class SVN(Vcs): ) def _log(self, refspec=None, maxres=None, filelist=None): - """ Retrieves log message and parses it""" + """Retrieves log message and parses it""" args = ['log', '--xml'] if refspec: diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index e7f19f16..eda377e3 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -183,8 +183,10 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes raise NotImplementedError def data_status_subpaths(self): - """Returns a dict indexed by subpaths not in sync with their status as values. - Paths are given relative to self.root""" + """ + Returns a dict indexed by subpaths not in sync with their status as values. + Paths are given relative to self.root + """ raise NotImplementedError def data_status_remote(self): @@ -250,29 +252,29 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method if wrootobj.content_loaded: has_vcschild = False - for fileobj in wrootobj.files_all: + for fsobj in wrootobj.files_all: if purge: - if fileobj.is_directory: - fileobj.vcsstatus = None - fileobj.vcs.__init__(fileobj) + if fsobj.is_directory: + fsobj.vcsstatus = None + fsobj.vcs.__init__(fsobj) else: - fileobj.vcsstatus = None + fsobj.vcsstatus = None continue - if fileobj.is_directory: - fileobj.vcs.check() - if not fileobj.vcs.track: + if fsobj.is_directory: + fsobj.vcs.check() + if not fsobj.vcs.track: continue - if fileobj.vcs.is_root_pointer: + if fsobj.vcs.is_root_pointer: has_vcschild = True else: - fileobj.vcsstatus = self.status_subpath( - os.path.join(wrootobj.realpath, fileobj.basename), + fsobj.vcsstatus = self.status_subpath( + os.path.join(wrootobj.realpath, fsobj.basename), is_directory=True, ) else: - fileobj.vcsstatus = self.status_subpath( - os.path.join(wrootobj.realpath, fileobj.basename)) + fsobj.vcsstatus = self.status_subpath( + os.path.join(wrootobj.realpath, fsobj.basename)) wrootobj.has_vcschild = has_vcschild # Remove dead directories @@ -379,45 +381,38 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut self.redraw = False self.roots = set() - def _hindered(self): - """Check for hinders""" - for column in self.ui.browser.columns: - if column.target and column.target.is_directory and column.target.flat: - return True - return False - def _is_targeted(self, dirobj): - ''' Check if dirobj is targeted ''' + """Check if dirobj is targeted""" if self.ui.browser.main_column and self.ui.browser.main_column.target == dirobj: return True return False - def _update_subroots(self, fileobjs): - ''' Update files ''' - if not fileobjs: + def _update_subroots(self, fsobjs): + """Update subroots""" + if not fsobjs: return False has_vcschild = False - for fileobj in fileobjs: - if not fileobj.is_directory or not fileobj.vcs or not fileobj.vcs.track: + for fsobj in fsobjs: + if not fsobj.is_directory or not fsobj.vcs or not fsobj.vcs.track: continue - rootvcs = fileobj.vcs.rootvcs - if fileobj.vcs.is_root_pointer: + rootvcs = fsobj.vcs.rootvcs + if fsobj.vcs.is_root_pointer: has_vcschild = True if not rootvcs.rootinit and not self._is_targeted(rootvcs.obj): self.roots.add(rootvcs.path) rootvcs.init_root() self.redraw = True - if fileobj.is_link: - fileobj.vcsstatus = rootvcs.obj.vcsstatus - fileobj.vcsremotestatus = rootvcs.obj.vcsremotestatus + if fsobj.is_link: + fsobj.vcsstatus = rootvcs.obj.vcsstatus + fsobj.vcsremotestatus = rootvcs.obj.vcsremotestatus self.redraw = True return has_vcschild def _queue_process(self): # pylint: disable=too-many-branches - """Process queue: Initialize roots under dirobj""" + """Process queue""" dirobjs = [] paths = set() self.roots.clear() @@ -460,8 +455,6 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut if column.target and column.target.is_directory: column.need_redraw = True self.ui.status.need_redraw = True - while self._hindered(): - time.sleep(0.01) self.ui.redraw() def wakeup(self, dirobj): @@ -478,20 +471,20 @@ from .svn import SVN # NOQA pylint: disable=wrong-import-position class BzrRoot(VcsRoot, Bzr): - ''' Bzr root ''' + """Bzr root""" pass class GitRoot(VcsRoot, Git): - ''' Git root ''' + """Git root""" pass class HgRoot(VcsRoot, Hg): - ''' Hg root ''' + """Hg root""" pass class SVNRoot(VcsRoot, SVN): - ''' SVN root ''' + """SVN root""" pass -- cgit 1.4.1-2-gfad0 From f6be81f599c87caca735231872443fc450ad304b Mon Sep 17 00:00:00 2001 From: nfnty Date: Wed, 10 Feb 2016 02:43:35 +0100 Subject: VCS: Fix has_vcschild directory load --- ranger/container/directory.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ranger/container/directory.py b/ranger/container/directory.py index fdaf1ee8..8ace0d85 100644 --- a/ranger/container/directory.py +++ b/ranger/container/directory.py @@ -306,6 +306,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): files = [] disk_usage = 0 + has_vcschild = False for name in filenames: try: file_lstat = os_lstat(name) @@ -332,9 +333,12 @@ class Directory(FileSystemObject, Accumulator, Loadable): else: item.relative_path = item.basename item.relative_path_lower = item.relative_path.lower() - if item.vcs and item.vcs.track and not item.vcs.is_root_pointer: - item.vcsstatus = item.vcs.rootvcs.status_subpath( - os.path.join(self.realpath, item.basename), is_directory=True) + if item.vcs and item.vcs.track: + if item.vcs.is_root_pointer: + has_vcschild = True + else: + item.vcsstatus = item.vcs.rootvcs.status_subpath( + os.path.join(self.realpath, item.basename), is_directory=True) else: item = File(name, preload=stats, path_is_abs=True, basename_is_rel_to=basename_is_rel_to) @@ -347,6 +351,7 @@ class Directory(FileSystemObject, Accumulator, Loadable): files.append(item) self.percent = 100 * len(files) // len(filenames) yield + self.has_vcschild = has_vcschild self.disk_usage = disk_usage self.filenames = filenames -- cgit 1.4.1-2-gfad0 From 5c2d64fe93d72da39ca7ffc6cf83f7e0395df524 Mon Sep 17 00:00:00 2001 From: nfnty Date: Wed, 10 Feb 2016 05:02:56 +0100 Subject: VCS: Fix reinitialization on repodir deletion --- ranger/ext/vcs/vcs.py | 60 +++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py index eda377e3..fd34c3fe 100644 --- a/ranger/ext/vcs/vcs.py +++ b/ranger/ext/vcs/vcs.py @@ -97,8 +97,7 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes self.track = True else: self.rootvcs = dirobj.fm.get_directory(self.root).vcs - self.rootvcs.check() - if self.rootvcs.root is None: + if self.rootvcs is None or self.rootvcs.root is None: return self.rootvcs.links |= self.links self.__class__ = globals()[self.REPOTYPES[self.repotype]['class']] @@ -155,16 +154,13 @@ class Vcs(object): # pylint: disable=too-many-instance-attributes return (None, None, None, None) - def check(self): - """Check health""" - if not self.in_repodir \ - and (not self.track or (not self.is_root and self._get_repotype(self.path)[0])): - self.__init__(self.obj) - return False - elif self.track and not os.path.exists(self.repodir): - self.rootvcs.update_tree(purge=True) # pylint: disable=no-member - return False - return True + def reinit(self): + """Reinit""" + if not self.in_repodir: + if not self.track \ + or (not self.is_root_pointer and self._get_repotype(self.obj.realpath)[0]) \ + or not os.path.exists(self.repodir): + self.__init__(self.obj) # Action interface @@ -232,11 +228,24 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method self.obj.vcsremotestatus = self.data_status_remote() self.obj.vcsstatus = self.data_status_root() except VcsError: - self.update_tree(purge=True) return False self.rootinit = True return True + def update_root(self): + """Update root state""" + try: + self.head = self.data_info(self.HEAD) + self.branch = self.data_branch() + self.status_subpaths = self.data_status_subpaths() + self.obj.vcsremotestatus = self.data_status_remote() + self.obj.vcsstatus = self._status_root() + except VcsError: + return False + self.rootinit = True + self.updatetime = time.time() + return True + def _update_walk(self, path, purge): # pylint: disable=too-many-branches """Update walk""" for wroot, wdirs, _ in os.walk(path): @@ -262,7 +271,7 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method continue if fsobj.is_directory: - fsobj.vcs.check() + fsobj.vcs.reinit() if not fsobj.vcs.track: continue if fsobj.vcs.is_root_pointer: @@ -306,21 +315,6 @@ class VcsRoot(Vcs): # pylint: disable=abstract-method if purge: self.__init__(self.obj) - def update_root(self): - """Update root state""" - try: - self.head = self.data_info(self.HEAD) - self.branch = self.data_branch() - self.status_subpaths = self.data_status_subpaths() - self.obj.vcsremotestatus = self.data_status_remote() - self.obj.vcsstatus = self._status_root() - except VcsError: - self.update_tree(purge=True) - return False - self.rootinit = True - self.updatetime = time.time() - return True - def check_outdated(self): """Check if root is outdated""" if self.updatetime is None: @@ -402,7 +396,8 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut has_vcschild = True if not rootvcs.rootinit and not self._is_targeted(rootvcs.obj): self.roots.add(rootvcs.path) - rootvcs.init_root() + if not rootvcs.init_root(): + rootvcs.update_tree(purge=True) self.redraw = True if fsobj.is_link: fsobj.vcsstatus = rootvcs.obj.vcsstatus @@ -428,13 +423,16 @@ class VcsThread(threading.Thread): # pylint: disable=too-many-instance-attribut continue paths.add(dirobj.path) + dirobj.vcs.reinit() if dirobj.vcs.track: rootvcs = dirobj.vcs.rootvcs if rootvcs.path not in self.roots and rootvcs.check_outdated(): self.roots.add(rootvcs.path) if rootvcs.update_root(): rootvcs.update_tree() - self.redraw = True + else: + rootvcs.update_tree(purge=True) + self.redraw = True has_vcschild = self._update_subroots(dirobj.files_all) -- cgit 1.4.1-2-gfad0 From cfc9f99e77eb00c1c9793a57af0a551f9b406605 Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 25 Feb 2016 08:36:05 +0100 Subject: VCS: Lazily initialize UI.vcsthread --- ranger/gui/ui.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index ff3ced57..41b57c2c 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -10,6 +10,7 @@ import threading from .displayable import DisplayableContainer from .mouse_event import MouseEvent from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY +from ranger.ext.lazy_property import lazy_property MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION @@ -221,7 +222,6 @@ class UI(DisplayableContainer): from ranger.gui.widgets.statusbar import StatusBar from ranger.gui.widgets.taskview import TaskView from ranger.gui.widgets.pager import Pager - from ranger.ext.vcs import VcsThread # Create a title bar self.titlebar = TitleBar(self.win) @@ -252,9 +252,13 @@ class UI(DisplayableContainer): self.pager.visible = False self.add_child(self.pager) - # Create VCS thread - self.vcsthread = VcsThread(self) - self.vcsthread.start() + @lazy_property + def vcsthread(self): + """VCS thread""" + from ranger.ext.vcs import VcsThread + thread = VcsThread(self) + thread.start() + return thread def redraw(self): """Redraw all widgets""" -- cgit 1.4.1-2-gfad0 From 4b9a296eeea704bd8adc12d06ef27d7b01be1c75 Mon Sep 17 00:00:00 2001 From: nfnty Date: Thu, 25 Feb 2016 08:46:50 +0100 Subject: VCS: statusbar: Fix date format --- ranger/gui/widgets/statusbar.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index fbd53855..33aa4296 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -178,14 +178,14 @@ class StatusBar(Widget): left.add(target.infostring.replace(" ", "")) left.add_space() - left.add(strftime(self.timeformat, - localtime(stat.st_mtime)), 'mtime') + left.add(strftime(self.timeformat, localtime(stat.st_mtime)), 'mtime') directory = target if target.is_directory else \ target.fm.get_directory(os.path.dirname(target.path)) if directory.vcs and directory.vcs.track: if directory.vcs.rootvcs.branch: - vcsinfo = '({0:s}: {1:s})'.format(directory.vcs.rootvcs.repotype, directory.vcs.rootvcs.branch) + vcsinfo = '({0:s}: {1:s})'.format( + directory.vcs.rootvcs.repotype, directory.vcs.rootvcs.branch) else: vcsinfo = '({0:s})'.format(directory.vcs.rootvcs.repotype) left.add_space() @@ -193,14 +193,15 @@ class StatusBar(Widget): left.add_space() if directory.vcs.rootvcs.obj.vcsremotestatus: - vcsstr, vcscol = self.vcsremotestatus_symb[directory.vcs.rootvcs.obj.vcsremotestatus] + vcsstr, vcscol = self.vcsremotestatus_symb[ + directory.vcs.rootvcs.obj.vcsremotestatus] left.add(vcsstr.strip(), 'vcsremote', *vcscol) if target.vcsstatus: vcsstr, vcscol = self.vcsstatus_symb[target.vcsstatus] left.add(vcsstr.strip(), 'vcsfile', *vcscol) if directory.vcs.rootvcs.head: left.add_space() - left.add(str(directory.vcs.rootvcs.head['date']), 'vcsdate') + left.add(directory.vcs.rootvcs.head['date'].strftime(self.timeformat), 'vcsdate') left.add_space() left.add(directory.vcs.rootvcs.head['summary'], 'vcscommit') -- cgit 1.4.1-2-gfad0