1"""Wrapper around git command-line tools.""" 2from __future__ import (absolute_import, division, print_function) 3__metaclass__ = type 4 5import re 6 7from . import types as t 8 9from .util import ( 10 SubprocessError, 11 raw_command, 12) 13 14 15class Git: 16 """Wrapper around git command-line tools.""" 17 def __init__(self, root=None): # type: (t.Optional[str]) -> None 18 self.git = 'git' 19 self.root = root 20 21 def get_diff(self, args, git_options=None): 22 """ 23 :type args: list[str] 24 :type git_options: list[str] | None 25 :rtype: list[str] 26 """ 27 cmd = ['diff'] + args 28 if git_options is None: 29 git_options = ['-c', 'core.quotePath='] 30 return self.run_git_split(git_options + cmd, '\n', str_errors='replace') 31 32 def get_diff_names(self, args): 33 """ 34 :type args: list[str] 35 :rtype: list[str] 36 """ 37 cmd = ['diff', '--name-only', '--no-renames', '-z'] + args 38 return self.run_git_split(cmd, '\0') 39 40 def get_submodule_paths(self): # type: () -> t.List[str] 41 """Return a list of submodule paths recursively.""" 42 cmd = ['submodule', 'status', '--recursive'] 43 output = self.run_git_split(cmd, '\n') 44 submodule_paths = [re.search(r'^.[0-9a-f]+ (?P<path>[^ ]+)', line).group('path') for line in output] 45 46 # status is returned for all submodules in the current git repository relative to the current directory 47 # when the current directory is not the root of the git repository this can yield relative paths which are not below the current directory 48 # this can occur when multiple collections are in a git repo and some collections are submodules when others are not 49 # specifying "." as the path to enumerate would limit results to the current directory, but can cause the git command to fail with the error: 50 # error: pathspec '.' did not match any file(s) known to git 51 # this can occur when the current directory contains no files tracked by git 52 # instead we'll filter out the relative paths, since we're only interested in those at or below the current directory 53 submodule_paths = [path for path in submodule_paths if not path.startswith('../')] 54 55 return submodule_paths 56 57 def get_file_names(self, args): 58 """ 59 :type args: list[str] 60 :rtype: list[str] 61 """ 62 cmd = ['ls-files', '-z'] + args 63 return self.run_git_split(cmd, '\0') 64 65 def get_branches(self): 66 """ 67 :rtype: list[str] 68 """ 69 cmd = ['for-each-ref', 'refs/heads/', '--format', '%(refname:strip=2)'] 70 return self.run_git_split(cmd) 71 72 def get_branch(self): 73 """ 74 :rtype: str 75 """ 76 cmd = ['symbolic-ref', '--short', 'HEAD'] 77 return self.run_git(cmd).strip() 78 79 def get_rev_list(self, commits=None, max_count=None): 80 """ 81 :type commits: list[str] | None 82 :type max_count: int | None 83 :rtype: list[str] 84 """ 85 cmd = ['rev-list'] 86 87 if commits: 88 cmd += commits 89 else: 90 cmd += ['HEAD'] 91 92 if max_count: 93 cmd += ['--max-count', '%s' % max_count] 94 95 return self.run_git_split(cmd) 96 97 def get_branch_fork_point(self, branch): 98 """ 99 :type branch: str 100 :rtype: str 101 """ 102 cmd = ['merge-base', '--fork-point', branch] 103 return self.run_git(cmd).strip() 104 105 def is_valid_ref(self, ref): 106 """ 107 :type ref: str 108 :rtype: bool 109 """ 110 cmd = ['show', ref] 111 try: 112 self.run_git(cmd, str_errors='replace') 113 return True 114 except SubprocessError: 115 return False 116 117 def run_git_split(self, cmd, separator=None, str_errors='strict'): 118 """ 119 :type cmd: list[str] 120 :type separator: str | None 121 :type str_errors: str 122 :rtype: list[str] 123 """ 124 output = self.run_git(cmd, str_errors=str_errors).strip(separator) 125 126 if not output: 127 return [] 128 129 return output.split(separator) 130 131 def run_git(self, cmd, str_errors='strict'): 132 """ 133 :type cmd: list[str] 134 :type str_errors: str 135 :rtype: str 136 """ 137 return raw_command([self.git] + cmd, cwd=self.root, capture=True, str_errors=str_errors)[0] 138