# This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. """Git module""" import os import re from datetime import datetime import json from .vcs import Vcs, VcsError class Git(Vcs): """VCS implementation for Git""" _status_translations = ( ('MADRC', ' ', 'staged'), (' MADRC', 'M', 'changed'), (' MARC', 'D', 'deleted'), ('D', 'DU', 'conflict'), ('A', 'AU', 'conflict'), ('U', 'ADU', 'conflict'), ('?', '?', 'untracked'), ('!', '!', 'ignored'), ) # Generic #--------------------------- def _git(self, args, path=None, silent=True, catchout=False, retbytes=False): """Run a git command""" return self._vcs(path or self.path, 'git', args, silent=silent, catchout=catchout, retbytes=retbytes) def _head_ref(self): """Returns HEAD reference""" return self._git(['symbolic-ref', self.HEAD], catchout=True, silent=True) or None def _remote_ref(self, ref): """Returns remote reference associated to given ref""" if ref is None: return None return self._git(['for-each-ref', '--format=%(upstream)', ref], catchout=True, silent=True) \ or None def _log(self, refspec=None, maxres=None, filelist=None): """Returns an array of dicts containing revision info for refspec""" args = [ '--no-pager', 'log', '--pretty={' '%x00short%x00:%x00%h%x00,' '%x00revid%x00:%x00%H%x00,' '%x00author%x00:%x00%an <%ae>%x00,' '%x00date%x00:%ct,' '%x00summary%x00:%x00%s%x00' '}' ] if refspec: args += ['-1', refspec] elif maxres: args += ['-{0:d}'.format(maxres)] if filelist: args += ['--'] + filelist try: log_raw = self._git(args, catchout=True)\ .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').splitlines() except VcsError: return [] log = [] for line in log_raw: line = json.loads(line) line['date'] = datetime.fromtimestamp(line['date']) log.append(line) return log def _git_status_translate(self, code): """Translate git status code""" for X, Y, status in self._status_translations: if code[0] in X and code[1] in Y: return status return 'unknown' # Action interface #--------------------------- def action_add(self, filelist=[]): self._git(['add', '--all'] + filelist) def action_reset(self, filelist=[]): self._git(['reset'] + filelist) # Data Interface #--------------------------- def data_status_root(self): statuses = set() # Paths with status skip = False for line in self._git(['status', '--porcelain', '-z'], catchout=True, retbytes=True).decode('utf-8').split('\x00')[:-1]: if skip: skip = False continue statuses.add(self._git_status_translate(line[:2])) if line.startswith('R'): skip = True for status in self.DIRSTATUSES: if status in statuses: return status return 'sync' def data_status_subpaths(self): statuses = {} # Ignored directories for line in self._git( ['ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'], catchout=True, retbytes=True ).decode('utf-8').split('\x00')[:-1]: if line.endswith('/'): statuses[os.path.normpath(line)] = 'ignored' # Empty directories for line in self._git( ['ls-files', '-z', '--others', '--directory', '--exclude-standard'], catchout=True, retbytes=True ).decode('utf-8').split('\x00')[:-1]: if line.endswith('/'): statuses[os.path.normpath(line)] = 'none' # Paths with status skip = False for line in self._git(['status', '--porcelain', '-z', '--ignored'], catchout=True, retbytes=True).decode('utf-8').split('\x00')[:-1]: if skip: skip = False continue statuses[os.path.normpath(line[3:])] = self._git_status_translate(line[:2]) if line.startswith('R'): skip = True return statuses def data_status_remote(self): try: head = self._head_ref() remote = self._remote_ref(head) except VcsError: head = remote = 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)