Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 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. |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 14 | """Helper functions to communicate with Git.""" |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 15 | |
| 16 | import datetime |
Haibo Huang | 0d3810f | 2018-08-31 20:44:25 -0700 | [diff] [blame] | 17 | import re |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 18 | import subprocess |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 19 | from pathlib import Path |
| 20 | from typing import Dict, List, Tuple |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 21 | |
Haibo Huang | 806c62b | 2020-09-25 15:32:36 -0700 | [diff] [blame] | 22 | import hashtags |
Chih-Hung Hsieh | b298bcd | 2020-08-17 23:13:59 -0700 | [diff] [blame] | 23 | import reviewers |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 24 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 25 | def _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) |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 28 | |
Haibo Huang | 9dcade4 | 2018-08-03 11:52:25 -0700 | [diff] [blame] | 29 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 30 | def fetch(proj_path: Path, remote_names: List[str]) -> None: |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 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 | """ |
Haibo Huang | 30e46de | 2021-02-18 19:36:36 -0800 | [diff] [blame] | 37 | _run(['git', 'fetch', '--tags', '--multiple'] + remote_names, cwd=proj_path) |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 38 | |
Haibo Huang | 9dcade4 | 2018-08-03 11:52:25 -0700 | [diff] [blame] | 39 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 40 | def add_remote(proj_path: Path, name: str, url: str) -> None: |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 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 | |
Haibo Huang | 9dcade4 | 2018-08-03 11:52:25 -0700 | [diff] [blame] | 50 | |
Haibo Huang | a08fb60 | 2020-05-29 16:24:13 -0700 | [diff] [blame] | 51 | def remove_remote(proj_path: Path, name: str) -> None: |
| 52 | """Removes a git remote.""" |
| 53 | _run(['git', 'remote', 'remove', name], cwd=proj_path) |
| 54 | |
| 55 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 56 | def list_remotes(proj_path: Path) -> Dict[str, str]: |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 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 | """ |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 65 | def parse_remote(line: str) -> Tuple[str, str]: |
| 66 | split = line.split() |
| 67 | return (split[0], split[1]) |
| 68 | |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 69 | out = _run(['git', 'remote', '-v'], proj_path) |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 70 | lines = out.splitlines() |
| 71 | return dict([parse_remote(line) for line in lines]) |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 72 | |
Haibo Huang | 9dcade4 | 2018-08-03 11:52:25 -0700 | [diff] [blame] | 73 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 74 | def 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 | |
| 79 | def get_commits_ahead(proj_path: Path, branch: str, |
| 80 | base_branch: str) -> List[str]: |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 81 | """Lists commits in `branch` but not `base_branch`.""" |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 82 | out = _run([ |
| 83 | 'git', 'rev-list', '--left-only', '--ancestry-path', '{}...{}'.format( |
| 84 | branch, base_branch) |
| 85 | ], proj_path) |
| 86 | return out.splitlines() |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 87 | |
Haibo Huang | 9dcade4 | 2018-08-03 11:52:25 -0700 | [diff] [blame] | 88 | |
ThiƩbaud Weksteen | 4ac289b | 2020-09-28 15:23:29 +0200 | [diff] [blame] | 89 | # pylint: disable=redefined-outer-name |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 90 | def get_commit_time(proj_path: Path, commit: str) -> datetime.datetime: |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 91 | """Gets commit time of one commit.""" |
| 92 | out = _run(['git', 'show', '-s', '--format=%ct', commit], cwd=proj_path) |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 93 | return datetime.datetime.fromtimestamp(int(out.strip())) |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 94 | |
Haibo Huang | 9dcade4 | 2018-08-03 11:52:25 -0700 | [diff] [blame] | 95 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 96 | def list_remote_branches(proj_path: Path, remote_name: str) -> List[str]: |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 97 | """Lists all branches for a remote.""" |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 98 | lines = _run(['git', 'branch', '-r'], cwd=proj_path).splitlines() |
Haibo Huang | 24950e7 | 2018-06-29 14:53:39 -0700 | [diff] [blame] | 99 | stripped = [line.strip() for line in lines] |
| 100 | remote_path = remote_name + '/' |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 101 | return [ |
Joel Galenson | c0d9333 | 2021-05-12 11:14:57 -0700 | [diff] [blame] | 102 | line[len(remote_path):] for line in stripped |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 103 | if line.startswith(remote_path) |
| 104 | ] |
Haibo Huang | 0d3810f | 2018-08-31 20:44:25 -0700 | [diff] [blame] | 105 | |
| 106 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 107 | def list_remote_tags(proj_path: Path, remote_name: str) -> List[str]: |
Haibo Huang | 0d3810f | 2018-08-31 20:44:25 -0700 | [diff] [blame] | 108 | """Lists all tags for a remote.""" |
Matthias Maennich | b959186 | 2020-09-22 22:44:37 +0100 | [diff] [blame] | 109 | regex = re.compile(r".*refs/tags/(?P<tag>[^\^]*).*") |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 110 | def parse_remote_tag(line: str) -> str: |
Matthias Maennich | b959186 | 2020-09-22 22:44:37 +0100 | [diff] [blame] | 111 | return regex.match(line).group("tag") |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 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] |
Haibo Huang | 0d3810f | 2018-08-31 20:44:25 -0700 | [diff] [blame] | 116 | return list(set(tags)) |
| 117 | |
| 118 | |
Joel Galenson | c0d9333 | 2021-05-12 11:14:57 -0700 | [diff] [blame] | 119 | def 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 | |
Haibo Huang | 0d3810f | 2018-08-31 20:44:25 -0700 | [diff] [blame] | 131 | COMMIT_PATTERN = r'^[a-f0-9]{40}$' |
| 132 | COMMIT_RE = re.compile(COMMIT_PATTERN) |
| 133 | |
| 134 | |
ThiƩbaud Weksteen | 4ac289b | 2020-09-28 15:23:29 +0200 | [diff] [blame] | 135 | # pylint: disable=redefined-outer-name |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 136 | def is_commit(commit: str) -> bool: |
Haibo Huang | cd2c612 | 2018-09-04 14:24:20 -0700 | [diff] [blame] | 137 | """Whether a string looks like a SHA1 hash.""" |
Haibo Huang | 0d3810f | 2018-08-31 20:44:25 -0700 | [diff] [blame] | 138 | return bool(COMMIT_RE.match(commit)) |
Haibo Huang | 0cabc2e | 2019-01-18 13:29:48 -0800 | [diff] [blame] | 139 | |
| 140 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 141 | def merge(proj_path: Path, branch: str) -> None: |
Haibo Huang | 0cabc2e | 2019-01-18 13:29:48 -0800 | [diff] [blame] | 142 | """Merges a branch.""" |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 143 | try: |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 144 | _run(['git', 'merge', branch, '--no-commit'], cwd=proj_path) |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 145 | except subprocess.CalledProcessError: |
| 146 | # Merge failed. Error is already written to console. |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 147 | _run(['git', 'merge', '--abort'], cwd=proj_path) |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 148 | raise |
| 149 | |
| 150 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 151 | def add_file(proj_path: Path, file_name: str) -> None: |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 152 | """Stages a file.""" |
| 153 | _run(['git', 'add', file_name], cwd=proj_path) |
| 154 | |
| 155 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 156 | def delete_branch(proj_path: Path, branch_name: str) -> None: |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 157 | """Force delete a branch.""" |
| 158 | _run(['git', 'branch', '-D', branch_name], cwd=proj_path) |
| 159 | |
| 160 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 161 | def start_branch(proj_path: Path, branch_name: str) -> None: |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 162 | """Starts a new repo branch.""" |
| 163 | _run(['repo', 'start', branch_name], cwd=proj_path) |
| 164 | |
| 165 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 166 | def commit(proj_path: Path, message: str) -> None: |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 167 | """Commits changes.""" |
| 168 | _run(['git', 'commit', '-m', message], cwd=proj_path) |
| 169 | |
| 170 | |
Haibo Huang | 329e681 | 2020-05-29 14:12:20 -0700 | [diff] [blame] | 171 | def checkout(proj_path: Path, branch_name: str) -> None: |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 172 | """Checkouts a branch.""" |
| 173 | _run(['git', 'checkout', branch_name], cwd=proj_path) |
| 174 | |
| 175 | |
Joel Galenson | 58eeae7 | 2021-04-07 12:44:31 -0700 | [diff] [blame] | 176 | def push(proj_path: Path, remote_name: str, has_errors: bool) -> None: |
Haibo Huang | c3c0cd4 | 2019-01-29 15:24:45 -0800 | [diff] [blame] | 177 | """Pushes change to remote.""" |
Haibo Huang | 806c62b | 2020-09-25 15:32:36 -0700 | [diff] [blame] | 178 | cmd = ['git', 'push', remote_name, 'HEAD:refs/for/master'] |
| 179 | if revs := reviewers.find_reviewers(str(proj_path)): |
| 180 | cmd.extend(['-o', revs]) |
| 181 | if tag := hashtags.find_hashtag(proj_path): |
| 182 | cmd.extend(['-o', 't=' + tag]) |
Joel Galenson | 58eeae7 | 2021-04-07 12:44:31 -0700 | [diff] [blame] | 183 | if has_errors: |
| 184 | cmd.extend(['-o', 'l=Verified-1']) |
Haibo Huang | 806c62b | 2020-09-25 15:32:36 -0700 | [diff] [blame] | 185 | _run(cmd, cwd=proj_path) |