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