From 0f4ae7b5ca1edc30b44f7a2ccbbfe3e2d4139908 Mon Sep 17 00:00:00 2001 From: hut Date: Sat, 16 Feb 2013 00:22:42 +0100 Subject: move ranger/vcs/* to ranger/ext/vcs/* --- ranger/config/commands.py | 8 +- ranger/ext/vcs/__init__.py | 24 +++ ranger/ext/vcs/bzr.py | 282 +++++++++++++++++++++++++++++++++++ ranger/ext/vcs/git.py | 315 +++++++++++++++++++++++++++++++++++++++ ranger/ext/vcs/hg.py | 283 +++++++++++++++++++++++++++++++++++ ranger/ext/vcs/vcs.py | 344 +++++++++++++++++++++++++++++++++++++++++++ ranger/fsobject/directory.py | 2 +- ranger/fsobject/fsobject.py | 2 +- ranger/vcs/__init__.py | 24 --- ranger/vcs/bzr.py | 282 ----------------------------------- ranger/vcs/git.py | 315 --------------------------------------- ranger/vcs/hg.py | 283 ----------------------------------- ranger/vcs/vcs.py | 344 ------------------------------------------- 13 files changed, 1254 insertions(+), 1254 deletions(-) create mode 100644 ranger/ext/vcs/__init__.py create mode 100644 ranger/ext/vcs/bzr.py create mode 100644 ranger/ext/vcs/git.py create mode 100644 ranger/ext/vcs/hg.py create mode 100644 ranger/ext/vcs/vcs.py delete mode 100644 ranger/vcs/__init__.py delete mode 100644 ranger/vcs/bzr.py delete mode 100644 ranger/vcs/git.py delete mode 100644 ranger/vcs/hg.py delete mode 100644 ranger/vcs/vcs.py diff --git a/ranger/config/commands.py b/ranger/config/commands.py index c98f36f9..94304f1c 100644 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1169,7 +1169,7 @@ class stage(Command): Stage selected files for the corresponding version control system """ def execute(self): - from ranger.vcs import VcsError + from ranger.ext.vcs import VcsError filelist = [f.path for f in self.fm.thistab.get_selection()] self.fm.thisdir.vcs_outdated = True @@ -1191,7 +1191,7 @@ class unstage(Command): Unstage selected files for the corresponding version control system """ def execute(self): - from ranger.vcs import VcsError + from ranger.ext.vcs import VcsError filelist = [f.path for f in self.fm.thistab.get_selection()] self.fm.thisdir.vcs_outdated = True @@ -1213,7 +1213,7 @@ class diff(Command): Displays a diff of selected files against last last commited version """ def execute(self): - from ranger.vcs import VcsError + from ranger.ext.vcs import VcsError import tempfile L = self.fm.thistab.get_selection() @@ -1241,7 +1241,7 @@ class log(Command): Displays the log of the current repo or files """ def execute(self): - from ranger.vcs import VcsError + from ranger.ext.vcs import VcsError import tempfile L = self.fm.thistab.get_selection() diff --git a/ranger/ext/vcs/__init__.py b/ranger/ext/vcs/__init__.py new file mode 100644 index 00000000..bb696178 --- /dev/null +++ b/ranger/ext/vcs/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# vcs - a python module to handle various version control systems +# Copyright 2011 Abdó Roig-Maranges +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from .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 new file mode 100644 index 00000000..a63eea54 --- /dev/null +++ b/ranger/ext/vcs/bzr.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# vcs - a python module to handle various version control systems +# Copyright 2012 Abdó Roig-Maranges +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import re +import shutil +from datetime import datetime + +from .vcs import Vcs, VcsError + + +class Bzr(Vcs): + vcsname = 'bzr' + HEAD="last:1" + + # Auxiliar stuff + #--------------------------- + + 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() + if len(rev) == 0: return None + + 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'] + if refspec: args = args + ["-r", refspec] + + if filelist: args = args + filelist + + raw = self._bzr(self.path, args, catchout=True, silent=True) + L = re.findall('-+$(.*?)^-', raw + '\n---', 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) + return log + + + def _hg_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' + + + + # 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 + #--------------------------- + + 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.""" + raw = self._bzr(self.path, ['status', '--short', '--no-classify'], catchout=True, bytes=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[p.strip()] = sta + return ret + + + def get_ignore_allfiles(self): + """Returns a set of all the ignored files in the repo""" + raw = self._bzr(self.path, ['ls', '--ignored'], catchout=True) + return set(raw.split('\n')) + + + # TODO: slow due to net access + def get_remote_status(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) + except VcsError: + remote = "" + + 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_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 [] + + 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) + 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 [] + +# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py new file mode 100644 index 00000000..b2e79a06 --- /dev/null +++ b/ranger/ext/vcs/git.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# vcs - a python module to handle various version control systems +# Copyright 2011, 2012 Abdó Roig-Maranges +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import re +import shutil +from datetime import datetime + +from .vcs import Vcs, VcsError + + +class Git(Vcs): + vcsname = 'git' + + # 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 _has_head(self): + """Checks whether repo has head""" + try: + self._git(self.path, ['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 + + + 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 + + + def _sanitize_rev(self, rev): + if rev == 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) + + 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) + 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' + + + + # Repo creation + #--------------------------- + + def init(self): + """Initializes a repo in current path""" + self._git(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._git(path, ['clone', src, name]) + self.update() + + + + # Action interface + #--------------------------- + + def commit(self, message): + """Commits with a given message""" + self._git(self.path, ['commit', '-m', 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']) + + + 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']) + + + def pull(self, br=None): + """Pulls from remote""" + if br: self._git(self.path, ['pull', br]) + else: self._git(self.path, ['pull']) + + + def push(self, br=None): + """Pushes to remote""" + if br: self._git(self.path, ['push', br]) + else: self._git(self.path, ['push']) + + + def checkout(self, rev): + """Checks out a branch or revision""" + self._git(self.path, ['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) + + + + # Data Interface + #--------------------------- + + 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.""" + 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: ret[m.group(2).strip()] = sta + else: ret[p.strip()] = sta + else: + ret[p.strip()] = sta + return ret + + + def get_ignore_allfiles(self): + """Returns a set of all the ignored files in the repo""" + raw = self._git(self.path, ['ls-files', '--others', '-i', '--exclude-standard'], catchout=True) + return set(raw.split('\n')) + + + def get_remote_status(self): + """Checks the status of the repo regarding sync state with remote branch""" + try: + head = self._head_ref() + 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" + + + def get_branch(self): + """Returns the current named branch, if this makes sense for the backend. None otherwise""" + try: + head = self._head_ref() + except VcsError: + head = None + + if head: + m = re.match('refs/heads/([^/]*)', head) + if m: return m.group(1).strip() + else: + return "detached" + + 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 = args + ['--'] + filelist + return self._git(self.path, 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) + + + 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 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 + 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 + rev = self._sanitize_rev(rev) + + return self._sanitize_rev(self._git(self.path, ['rev-parse', rev], catchout=True)) + + + def get_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 + + 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) + 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._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') + else: + return [] + +# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py new file mode 100644 index 00000000..08e5753b --- /dev/null +++ b/ranger/ext/vcs/hg.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# vcs - a python module to handle various version control systems +# Copyright 2011, 2012 Abdó Roig-Maranges +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +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): + vcsname = 'hg' + HEAD = 'tip' + # Auxiliar stuff + #--------------------------- + + 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() + if len(rev) == 0: return None + if rev[-1] == '+': rev = rev[:-1] + + try: + if int(rev) == 0: return None + except: + pass + + return rev + + + 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)] + + 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) + + 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) + 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' + + + + # 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_allfiles().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 + #--------------------------- + + 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.""" + raw = self._hg(self.path, ['status'], catchout=True, bytes=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[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_remote_status(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._hg(self.path, ['outgoing'], silent=True) + except: + ahead = False + + try: + self._hg(self.path, ['incoming'], 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._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 + 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) + 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 [] + + +# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py new file mode 100644 index 00000000..4fca48c7 --- /dev/null +++ b/ranger/ext/vcs/vcs.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# vcs - a python module to handle various version control systems +# Copyright 2011, 2012 Abdó Roig-Maranges +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import subprocess +from datetime import datetime + + +class VcsError(Exception): + pass + + +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 + 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 + + 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 + # 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 + self.repo_types = {'git': Git, 'hg': Hg, 'bzr': Bzr} + + self.path = os.path.expanduser(path) + self.status = {} + self.ignored = set() + self.root = None + + self.update(vcstype=vcstype) + + + # 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 + 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() + 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))) + + + def _path_contains(self, parent, path): + """Checks wether path is an object belonging to the subtree in parent""" + if parent == path: return true + return os.path.commonprefix([parent, path]) == parent + + + # Object manipulation + #--------------------------- + # 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): + """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 + return None + + + def get_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 + + + # Repo creation + #--------------------------- + + def init(self, repotype): + """Initializes a repo in current path""" + if not repotype in self.repo_types: + raise VcsError("Unrecognized repo type %s" % repotype) + + if not os.path.exists(self.path): os.makedirs(self.path) + rt = self.repo_types[repotype] + try: + self.__class__ = rt + 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) + + if not os.path.exists(self.path): os.makedirs(self.path) + rt = self.repo_types[repotype] + try: + self.__class__ = rt + self.clone(src) + except: + self.__class__ = Vcs + raise + + + # 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): + """Pulls from remote""" + raise NotImplementedError + + + def push(self): + """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 + + + 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) + + if os.path.commonprefix([self.root, path]) == self.root: + prel = os.path.relpath(path, self.root) + if prel in self.ignored: return "ignored" + 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))]) | set(['sync']) + for st in self.FILE_STATUS: + if st in sts: return st + else: + if prel in self.status: return self.status[prel] + else: return "sync" + else: + return "none" + + + 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 {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 + + + 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.""" + raise NotImplementedError + + + def get_ignore_allfiles(self): + """Returns a set of all the ignored files in the repo""" + 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/fsobject/directory.py b/ranger/fsobject/directory.py index bd266982..e0b1daa6 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -16,8 +16,8 @@ from ranger.core.shared import SettingsAware from ranger.ext.accumulator import Accumulator from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable +from ranger.ext.vcs import VcsError from ranger.container.settingobject import LocalSettingObject -from ranger.vcs import VcsError def sort_by_basename(path): """returns path.basename (for sorting)""" diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py index c044dbdb..f8a8d162 100644 --- a/ranger/fsobject/fsobject.py +++ b/ranger/fsobject/fsobject.py @@ -18,7 +18,7 @@ from ranger.ext.shell_escape import shell_escape from ranger.ext.spawn import spawn from ranger.ext.lazy_property import lazy_property from ranger.ext.human_readable import human_readable -from ranger.vcs import Vcs +from ranger.ext.vcs import Vcs if hasattr(str, 'maketrans'): maketrans = str.maketrans diff --git a/ranger/vcs/__init__.py b/ranger/vcs/__init__.py deleted file mode 100644 index bb696178..00000000 --- a/ranger/vcs/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# vcs - a python module to handle various version control systems -# Copyright 2011 Abdó Roig-Maranges -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from .vcs import VcsError, Vcs - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/vcs/bzr.py b/ranger/vcs/bzr.py deleted file mode 100644 index a63eea54..00000000 --- a/ranger/vcs/bzr.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# vcs - a python module to handle various version control systems -# Copyright 2012 Abdó Roig-Maranges -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import re -import shutil -from datetime import datetime - -from .vcs import Vcs, VcsError - - -class Bzr(Vcs): - vcsname = 'bzr' - HEAD="last:1" - - # Auxiliar stuff - #--------------------------- - - 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() - if len(rev) == 0: return None - - 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'] - if refspec: args = args + ["-r", refspec] - - if filelist: args = args + filelist - - raw = self._bzr(self.path, args, catchout=True, silent=True) - L = re.findall('-+$(.*?)^-', raw + '\n---', 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) - return log - - - def _hg_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' - - - - # 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 - #--------------------------- - - 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.""" - raw = self._bzr(self.path, ['status', '--short', '--no-classify'], catchout=True, bytes=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[p.strip()] = sta - return ret - - - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo""" - raw = self._bzr(self.path, ['ls', '--ignored'], catchout=True) - return set(raw.split('\n')) - - - # TODO: slow due to net access - def get_remote_status(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) - except VcsError: - remote = "" - - 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_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 [] - - 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) - 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 [] - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/vcs/git.py b/ranger/vcs/git.py deleted file mode 100644 index b2e79a06..00000000 --- a/ranger/vcs/git.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# vcs - a python module to handle various version control systems -# Copyright 2011, 2012 Abdó Roig-Maranges -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import re -import shutil -from datetime import datetime - -from .vcs import Vcs, VcsError - - -class Git(Vcs): - vcsname = 'git' - - # 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 _has_head(self): - """Checks whether repo has head""" - try: - self._git(self.path, ['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 - - - 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 - - - def _sanitize_rev(self, rev): - if rev == 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) - - 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) - 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' - - - - # Repo creation - #--------------------------- - - def init(self): - """Initializes a repo in current path""" - self._git(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._git(path, ['clone', src, name]) - self.update() - - - - # Action interface - #--------------------------- - - def commit(self, message): - """Commits with a given message""" - self._git(self.path, ['commit', '-m', 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']) - - - 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']) - - - def pull(self, br=None): - """Pulls from remote""" - if br: self._git(self.path, ['pull', br]) - else: self._git(self.path, ['pull']) - - - def push(self, br=None): - """Pushes to remote""" - if br: self._git(self.path, ['push', br]) - else: self._git(self.path, ['push']) - - - def checkout(self, rev): - """Checks out a branch or revision""" - self._git(self.path, ['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) - - - - # Data Interface - #--------------------------- - - 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.""" - 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: ret[m.group(2).strip()] = sta - else: ret[p.strip()] = sta - else: - ret[p.strip()] = sta - return ret - - - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo""" - raw = self._git(self.path, ['ls-files', '--others', '-i', '--exclude-standard'], catchout=True) - return set(raw.split('\n')) - - - def get_remote_status(self): - """Checks the status of the repo regarding sync state with remote branch""" - try: - head = self._head_ref() - 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" - - - def get_branch(self): - """Returns the current named branch, if this makes sense for the backend. None otherwise""" - try: - head = self._head_ref() - except VcsError: - head = None - - if head: - m = re.match('refs/heads/([^/]*)', head) - if m: return m.group(1).strip() - else: - return "detached" - - 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 = args + ['--'] + filelist - return self._git(self.path, 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) - - - 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 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 - 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 - rev = self._sanitize_rev(rev) - - return self._sanitize_rev(self._git(self.path, ['rev-parse', rev], catchout=True)) - - - def get_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 - - 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) - 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._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') - else: - return [] - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/vcs/hg.py b/ranger/vcs/hg.py deleted file mode 100644 index 08e5753b..00000000 --- a/ranger/vcs/hg.py +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# vcs - a python module to handle various version control systems -# Copyright 2011, 2012 Abdó Roig-Maranges -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -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): - vcsname = 'hg' - HEAD = 'tip' - # Auxiliar stuff - #--------------------------- - - 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() - if len(rev) == 0: return None - if rev[-1] == '+': rev = rev[:-1] - - try: - if int(rev) == 0: return None - except: - pass - - return rev - - - 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)] - - 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) - - 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) - 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' - - - - # 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_allfiles().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 - #--------------------------- - - 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.""" - raw = self._hg(self.path, ['status'], catchout=True, bytes=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[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_remote_status(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._hg(self.path, ['outgoing'], silent=True) - except: - ahead = False - - try: - self._hg(self.path, ['incoming'], 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._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 - 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) - 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 [] - - -# vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=4:textwidth=80 diff --git a/ranger/vcs/vcs.py b/ranger/vcs/vcs.py deleted file mode 100644 index 4fca48c7..00000000 --- a/ranger/vcs/vcs.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# vcs - a python module to handle various version control systems -# Copyright 2011, 2012 Abdó Roig-Maranges -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import subprocess -from datetime import datetime - - -class VcsError(Exception): - pass - - -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 - 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 - - 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 - # 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 - self.repo_types = {'git': Git, 'hg': Hg, 'bzr': Bzr} - - self.path = os.path.expanduser(path) - self.status = {} - self.ignored = set() - self.root = None - - self.update(vcstype=vcstype) - - - # 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 - 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() - 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))) - - - def _path_contains(self, parent, path): - """Checks wether path is an object belonging to the subtree in parent""" - if parent == path: return true - return os.path.commonprefix([parent, path]) == parent - - - # Object manipulation - #--------------------------- - # 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): - """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 - return None - - - def get_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 - - - # Repo creation - #--------------------------- - - def init(self, repotype): - """Initializes a repo in current path""" - if not repotype in self.repo_types: - raise VcsError("Unrecognized repo type %s" % repotype) - - if not os.path.exists(self.path): os.makedirs(self.path) - rt = self.repo_types[repotype] - try: - self.__class__ = rt - 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) - - if not os.path.exists(self.path): os.makedirs(self.path) - rt = self.repo_types[repotype] - try: - self.__class__ = rt - self.clone(src) - except: - self.__class__ = Vcs - raise - - - # 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): - """Pulls from remote""" - raise NotImplementedError - - - def push(self): - """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 - - - 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) - - if os.path.commonprefix([self.root, path]) == self.root: - prel = os.path.relpath(path, self.root) - if prel in self.ignored: return "ignored" - 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))]) | set(['sync']) - for st in self.FILE_STATUS: - if st in sts: return st - else: - if prel in self.status: return self.status[prel] - else: return "sync" - else: - return "none" - - - 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 {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 - - - 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.""" - raise NotImplementedError - - - def get_ignore_allfiles(self): - """Returns a set of all the ignored files in the repo""" - 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) -- cgit 1.4.1-2-gfad0