• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
19from pathlib import Path
20from typing import Dict, List, Tuple
21
22import hashtags
23import reviewers
24
25def _run(cmd: List[str], cwd: Path) -> str:
26    """Runs a command and returns its output."""
27    return subprocess.check_output(cmd, text=True, cwd=cwd)
28
29
30def fetch(proj_path: Path, remote_names: List[str]) -> None:
31    """Runs git fetch.
32
33    Args:
34        proj_path: Path to Git repository.
35        remote_names: Array of string to specify remote names.
36    """
37    _run(['git', 'fetch', '--tags', '--multiple'] + remote_names, cwd=proj_path)
38
39
40def add_remote(proj_path: Path, name: str, url: str) -> None:
41    """Adds a git remote.
42
43    Args:
44        proj_path: Path to Git repository.
45        name: Name of the new remote.
46        url: Url of the new remote.
47    """
48    _run(['git', 'remote', 'add', name, url], cwd=proj_path)
49
50
51def remove_remote(proj_path: Path, name: str) -> None:
52    """Removes a git remote."""
53    _run(['git', 'remote', 'remove', name], cwd=proj_path)
54
55
56def list_remotes(proj_path: Path) -> Dict[str, str]:
57    """Lists all Git remotes.
58
59    Args:
60        proj_path: Path to Git repository.
61
62    Returns:
63        A dict from remote name to remote url.
64    """
65    def parse_remote(line: str) -> Tuple[str, str]:
66        split = line.split()
67        return (split[0], split[1])
68
69    out = _run(['git', 'remote', '-v'], proj_path)
70    lines = out.splitlines()
71    return dict([parse_remote(line) for line in lines])
72
73
74def get_sha_for_branch(proj_path: Path, branch: str):
75    """Gets the hash SHA for a branch."""
76    return _run(['git', 'rev-parse', branch], proj_path).strip()
77
78
79def get_commits_ahead(proj_path: Path, branch: str,
80                      base_branch: str) -> List[str]:
81    """Lists commits in `branch` but not `base_branch`."""
82    out = _run([
83        'git', 'rev-list', '--left-only', '--ancestry-path', '{}...{}'.format(
84            branch, base_branch)
85    ], proj_path)
86    return out.splitlines()
87
88
89# pylint: disable=redefined-outer-name
90def get_commit_time(proj_path: Path, commit: str) -> datetime.datetime:
91    """Gets commit time of one commit."""
92    out = _run(['git', 'show', '-s', '--format=%ct', commit], cwd=proj_path)
93    return datetime.datetime.fromtimestamp(int(out.strip()))
94
95
96def list_remote_branches(proj_path: Path, remote_name: str) -> List[str]:
97    """Lists all branches for a remote."""
98    lines = _run(['git', 'branch', '-r'], cwd=proj_path).splitlines()
99    stripped = [line.strip() for line in lines]
100    remote_path = remote_name + '/'
101    return [
102        line[len(remote_path):] for line in stripped
103        if line.startswith(remote_path)
104    ]
105
106
107def list_remote_tags(proj_path: Path, remote_name: str) -> List[str]:
108    """Lists all tags for a remote."""
109    regex = re.compile(r".*refs/tags/(?P<tag>[^\^]*).*")
110    def parse_remote_tag(line: str) -> str:
111        return regex.match(line).group("tag")
112
113    lines = _run(['git', "ls-remote", "--tags", remote_name],
114                 cwd=proj_path).splitlines()
115    tags = [parse_remote_tag(line) for line in lines]
116    return list(set(tags))
117
118
119def get_default_branch(proj_path: Path, remote_name: str) -> str:
120    """Gets the name of the upstream branch to use."""
121    branches_to_try = ['master', 'main']
122    remote_branches = list_remote_branches(proj_path, remote_name)
123    for branch in branches_to_try:
124        if branch in remote_branches:
125            return branch
126    # We couldn't find any of the branches we expected.
127    # Default to 'master', although nothing will work well.
128    return 'master'
129
130
131COMMIT_PATTERN = r'^[a-f0-9]{40}$'
132COMMIT_RE = re.compile(COMMIT_PATTERN)
133
134
135# pylint: disable=redefined-outer-name
136def is_commit(commit: str) -> bool:
137    """Whether a string looks like a SHA1 hash."""
138    return bool(COMMIT_RE.match(commit))
139
140
141def merge(proj_path: Path, branch: str) -> None:
142    """Merges a branch."""
143    try:
144        _run(['git', 'merge', branch, '--no-commit'], cwd=proj_path)
145    except subprocess.CalledProcessError as err:
146        if hasattr(err, "output"):
147            print(err.output)
148        _run(['git', 'merge', '--abort'], cwd=proj_path)
149        raise
150
151
152def add_file(proj_path: Path, file_name: str) -> None:
153    """Stages a file."""
154    _run(['git', 'add', file_name], cwd=proj_path)
155
156
157def remove_gitmodules(proj_path: Path) -> None:
158    """Deletes .gitmodules files."""
159    _run(['find', '.', '-name', '.gitmodules', '-delete'], cwd=proj_path)
160
161
162def delete_branch(proj_path: Path, branch_name: str) -> None:
163    """Force delete a branch."""
164    _run(['git', 'branch', '-D', branch_name], cwd=proj_path)
165
166
167def start_branch(proj_path: Path, branch_name: str) -> None:
168    """Starts a new repo branch."""
169    _run(['repo', 'start', branch_name], cwd=proj_path)
170
171
172def commit(proj_path: Path, message: str) -> None:
173    """Commits changes."""
174    _run(['git', 'commit', '-m', message], cwd=proj_path)
175
176
177def checkout(proj_path: Path, branch_name: str) -> None:
178    """Checkouts a branch."""
179    _run(['git', 'checkout', branch_name], cwd=proj_path)
180
181
182def push(proj_path: Path, remote_name: str, has_errors: bool) -> None:
183    """Pushes change to remote."""
184    cmd = ['git', 'push', remote_name, 'HEAD:refs/for/master']
185    if revs := reviewers.find_reviewers(str(proj_path)):
186        cmd.extend(['-o', revs])
187    if tag := hashtags.find_hashtag(proj_path):
188        cmd.extend(['-o', 't=' + tag])
189    if has_errors:
190        cmd.extend(['-o', 'l=Verified-1'])
191    _run(cmd, cwd=proj_path)
192