1# Copyright (C) 2018 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the 'License');
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an 'AS IS' BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14'''Helper functions to communicate with Git.'''
15
16import datetime
17import re
18import subprocess
19
20
21def _run(cmd, cwd, redirect=True):
22    """Runs a command with stdout and stderr redirected."""
23    out = subprocess.PIPE if redirect else None
24    return subprocess.run(cmd, stdout=out, stderr=out,
25                          check=True, cwd=cwd)
26
27
28def fetch(proj_path, remote_names):
29    """Runs git fetch.
30
31    Args:
32        proj_path: Path to Git repository.
33        remote_names: Array of string to specify remote names.
34    """
35    _run(['git', 'fetch', '--multiple'] + remote_names, cwd=proj_path)
36
37
38def add_remote(proj_path, name, url):
39    """Adds a git remote.
40
41    Args:
42        proj_path: Path to Git repository.
43        name: Name of the new remote.
44        url: Url of the new remote.
45    """
46    _run(['git', 'remote', 'add', name, url], cwd=proj_path)
47
48
49def list_remotes(proj_path):
50    """Lists all Git remotes.
51
52    Args:
53        proj_path: Path to Git repository.
54
55    Returns:
56        A dict from remote name to remote url.
57    """
58    out = _run(['git', 'remote', '-v'], proj_path)
59    lines = out.stdout.decode('utf-8').splitlines()
60    return dict([line.split()[0:2] for line in lines])
61
62
63def get_commits_ahead(proj_path, branch, base_branch):
64    """Lists commits in `branch` but not `base_branch`."""
65    out = _run(['git', 'rev-list', '--left-only', '--ancestry-path',
66                '{}...{}'.format(branch, base_branch)],
67               proj_path)
68    return out.stdout.decode('utf-8').splitlines()
69
70
71def get_commit_time(proj_path, commit):
72    """Gets commit time of one commit."""
73    out = _run(['git', 'show', '-s', '--format=%ct', commit], cwd=proj_path)
74    return datetime.datetime.fromtimestamp(int(out.stdout))
75
76
77def list_remote_branches(proj_path, remote_name):
78    """Lists all branches for a remote."""
79    out = _run(['git', 'branch', '-r'], cwd=proj_path)
80    lines = out.stdout.decode('utf-8').splitlines()
81    stripped = [line.strip() for line in lines]
82    remote_path = remote_name + '/'
83    remote_path_len = len(remote_path)
84    return [line[remote_path_len:] for line in stripped
85            if line.startswith(remote_path)]
86
87
88def _parse_remote_tag(line):
89    tag_prefix = 'refs/tags/'
90    tag_suffix = '^{}'
91    try:
92        line = line[line.index(tag_prefix):]
93    except ValueError:
94        return None
95    line = line[len(tag_prefix):]
96    if line.endswith(tag_suffix):
97        line = line[:-len(tag_suffix)]
98    return line
99
100
101def list_remote_tags(proj_path, remote_name):
102    """Lists all tags for a remote."""
103    out = _run(['git', "ls-remote", "--tags", remote_name],
104               cwd=proj_path)
105    lines = out.stdout.decode('utf-8').splitlines()
106    tags = [_parse_remote_tag(line) for line in lines]
107    return list(set(tags))
108
109
110COMMIT_PATTERN = r'^[a-f0-9]{40}$'
111COMMIT_RE = re.compile(COMMIT_PATTERN)
112
113
114def is_commit(commit):
115    """Whether a string looks like a SHA1 hash."""
116    return bool(COMMIT_RE.match(commit))
117
118
119def merge(proj_path, branch):
120    """Merges a branch."""
121    try:
122        out = _run(['git', 'merge', branch, '--no-commit'],
123                   cwd=proj_path)
124    except subprocess.CalledProcessError:
125        # Merge failed. Error is already written to console.
126        subprocess.run(['git', 'merge', '--abort'], cwd=proj_path)
127        raise
128
129
130def add_file(proj_path, file_name):
131    """Stages a file."""
132    _run(['git', 'add', file_name], cwd=proj_path)
133
134
135def delete_branch(proj_path, branch_name):
136    """Force delete a branch."""
137    _run(['git', 'branch', '-D', branch_name], cwd=proj_path)
138
139
140def start_branch(proj_path, branch_name):
141    """Starts a new repo branch."""
142    _run(['repo', 'start', branch_name], cwd=proj_path)
143
144
145def commit(proj_path, message):
146    """Commits changes."""
147    _run(['git', 'commit', '-m', message], cwd=proj_path)
148
149
150def checkout(proj_path, branch_name):
151    """Checkouts a branch."""
152    _run(['git', 'checkout', branch_name], cwd=proj_path)
153
154
155def push(proj_path, remote_name):
156    """Pushes change to remote."""
157    return _run(['git', 'push', remote_name, 'HEAD:refs/for/master'],
158                cwd=proj_path, redirect=False)
159