diff options
author | nfnty <git@nfnty.se> | 2015-10-05 14:50:23 +0200 |
---|---|---|
committer | nfnty <git@nfnty.se> | 2016-02-08 04:43:03 +0100 |
commit | 96dd8db40de076e751adcba65931b981438c1de2 (patch) | |
tree | a665dd34912fd5c406fc0b2194083a99089a39a6 | |
parent | e7e867685eaebb0d12e9e69b74b18cb565b84f10 (diff) | |
download | ranger-96dd8db40de076e751adcba65931b981438c1de2.tar.gz |
VCS: Git
-rw-r--r-- | ranger/container/directory.py | 28 | ||||
-rw-r--r-- | ranger/container/fsobject.py | 83 | ||||
-rw-r--r-- | ranger/ext/vcs/__init__.py | 4 | ||||
-rw-r--r-- | ranger/ext/vcs/bzr.py | 2 | ||||
-rw-r--r-- | ranger/ext/vcs/git.py | 312 | ||||
-rw-r--r-- | ranger/ext/vcs/hg.py | 2 | ||||
-rw-r--r-- | ranger/ext/vcs/svn.py | 2 | ||||
-rw-r--r-- | ranger/ext/vcs/vcs.py | 351 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 22 | ||||
-rw-r--r-- | 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 <abdo.roig@gmail.com>, 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 |