blob: f96a600327e49e2f02b865efcd561698c07af373 [file] [log] [blame]
Haibo Huang24950e72018-06-29 14:53:39 -07001# 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 Huang329e6812020-05-29 14:12:20 -070014"""Helper functions to communicate with Git."""
Haibo Huang24950e72018-06-29 14:53:39 -070015
16import datetime
Haibo Huang0d3810f2018-08-31 20:44:25 -070017import re
Haibo Huang24950e72018-06-29 14:53:39 -070018import subprocess
Haibo Huang329e6812020-05-29 14:12:20 -070019from pathlib import Path
20from typing import Dict, List, Tuple
Haibo Huang24950e72018-06-29 14:53:39 -070021
Haibo Huang806c62b2020-09-25 15:32:36 -070022import hashtags
Chih-Hung Hsiehb298bcd2020-08-17 23:13:59 -070023import reviewers
Haibo Huang24950e72018-06-29 14:53:39 -070024
Haibo Huang329e6812020-05-29 14:12:20 -070025def _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 Huang24950e72018-06-29 14:53:39 -070028
Haibo Huang9dcade42018-08-03 11:52:25 -070029
Haibo Huang329e6812020-05-29 14:12:20 -070030def fetch(proj_path: Path, remote_names: List[str]) -> None:
Haibo Huang24950e72018-06-29 14:53:39 -070031 """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 Huang30e46de2021-02-18 19:36:36 -080037 _run(['git', 'fetch', '--tags', '--multiple'] + remote_names, cwd=proj_path)
Haibo Huang24950e72018-06-29 14:53:39 -070038
Haibo Huang9dcade42018-08-03 11:52:25 -070039
Haibo Huang329e6812020-05-29 14:12:20 -070040def add_remote(proj_path: Path, name: str, url: str) -> None:
Haibo Huang24950e72018-06-29 14:53:39 -070041 """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 Huang9dcade42018-08-03 11:52:25 -070050
Haibo Huanga08fb602020-05-29 16:24:13 -070051def 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 Huang329e6812020-05-29 14:12:20 -070056def list_remotes(proj_path: Path) -> Dict[str, str]:
Haibo Huang24950e72018-06-29 14:53:39 -070057 """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 Huang329e6812020-05-29 14:12:20 -070065 def parse_remote(line: str) -> Tuple[str, str]:
66 split = line.split()
67 return (split[0], split[1])
68
Haibo Huang24950e72018-06-29 14:53:39 -070069 out = _run(['git', 'remote', '-v'], proj_path)
Haibo Huang329e6812020-05-29 14:12:20 -070070 lines = out.splitlines()
71 return dict([parse_remote(line) for line in lines])
Haibo Huang24950e72018-06-29 14:53:39 -070072
Haibo Huang9dcade42018-08-03 11:52:25 -070073
Haibo Huang329e6812020-05-29 14:12:20 -070074def 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]:
Haibo Huang24950e72018-06-29 14:53:39 -070081 """Lists commits in `branch` but not `base_branch`."""
Haibo Huang329e6812020-05-29 14:12:20 -070082 out = _run([
83 'git', 'rev-list', '--left-only', '--ancestry-path', '{}...{}'.format(
84 branch, base_branch)
85 ], proj_path)
86 return out.splitlines()
Haibo Huang24950e72018-06-29 14:53:39 -070087
Haibo Huang9dcade42018-08-03 11:52:25 -070088
ThiƩbaud Weksteen4ac289b2020-09-28 15:23:29 +020089# pylint: disable=redefined-outer-name
Haibo Huang329e6812020-05-29 14:12:20 -070090def get_commit_time(proj_path: Path, commit: str) -> datetime.datetime:
Haibo Huang24950e72018-06-29 14:53:39 -070091 """Gets commit time of one commit."""
92 out = _run(['git', 'show', '-s', '--format=%ct', commit], cwd=proj_path)
Haibo Huang329e6812020-05-29 14:12:20 -070093 return datetime.datetime.fromtimestamp(int(out.strip()))
Haibo Huang24950e72018-06-29 14:53:39 -070094
Haibo Huang9dcade42018-08-03 11:52:25 -070095
Haibo Huang329e6812020-05-29 14:12:20 -070096def list_remote_branches(proj_path: Path, remote_name: str) -> List[str]:
Haibo Huang24950e72018-06-29 14:53:39 -070097 """Lists all branches for a remote."""
Haibo Huang329e6812020-05-29 14:12:20 -070098 lines = _run(['git', 'branch', '-r'], cwd=proj_path).splitlines()
Haibo Huang24950e72018-06-29 14:53:39 -070099 stripped = [line.strip() for line in lines]
100 remote_path = remote_name + '/'
Haibo Huang329e6812020-05-29 14:12:20 -0700101 return [
Joel Galensonc0d93332021-05-12 11:14:57 -0700102 line[len(remote_path):] for line in stripped
Haibo Huang329e6812020-05-29 14:12:20 -0700103 if line.startswith(remote_path)
104 ]
Haibo Huang0d3810f2018-08-31 20:44:25 -0700105
106
Haibo Huang329e6812020-05-29 14:12:20 -0700107def list_remote_tags(proj_path: Path, remote_name: str) -> List[str]:
Haibo Huang0d3810f2018-08-31 20:44:25 -0700108 """Lists all tags for a remote."""
Matthias Maennichb9591862020-09-22 22:44:37 +0100109 regex = re.compile(r".*refs/tags/(?P<tag>[^\^]*).*")
Haibo Huang329e6812020-05-29 14:12:20 -0700110 def parse_remote_tag(line: str) -> str:
Matthias Maennichb9591862020-09-22 22:44:37 +0100111 return regex.match(line).group("tag")
Haibo Huang329e6812020-05-29 14:12:20 -0700112
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 Huang0d3810f2018-08-31 20:44:25 -0700116 return list(set(tags))
117
118
Joel Galensonc0d93332021-05-12 11:14:57 -0700119def 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 Huang0d3810f2018-08-31 20:44:25 -0700131COMMIT_PATTERN = r'^[a-f0-9]{40}$'
132COMMIT_RE = re.compile(COMMIT_PATTERN)
133
134
ThiƩbaud Weksteen4ac289b2020-09-28 15:23:29 +0200135# pylint: disable=redefined-outer-name
Haibo Huang329e6812020-05-29 14:12:20 -0700136def is_commit(commit: str) -> bool:
Haibo Huangcd2c6122018-09-04 14:24:20 -0700137 """Whether a string looks like a SHA1 hash."""
Haibo Huang0d3810f2018-08-31 20:44:25 -0700138 return bool(COMMIT_RE.match(commit))
Haibo Huang0cabc2e2019-01-18 13:29:48 -0800139
140
Haibo Huang329e6812020-05-29 14:12:20 -0700141def merge(proj_path: Path, branch: str) -> None:
Haibo Huang0cabc2e2019-01-18 13:29:48 -0800142 """Merges a branch."""
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800143 try:
Haibo Huang329e6812020-05-29 14:12:20 -0700144 _run(['git', 'merge', branch, '--no-commit'], cwd=proj_path)
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800145 except subprocess.CalledProcessError:
146 # Merge failed. Error is already written to console.
Haibo Huang329e6812020-05-29 14:12:20 -0700147 _run(['git', 'merge', '--abort'], cwd=proj_path)
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800148 raise
149
150
Haibo Huang329e6812020-05-29 14:12:20 -0700151def add_file(proj_path: Path, file_name: str) -> None:
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800152 """Stages a file."""
153 _run(['git', 'add', file_name], cwd=proj_path)
154
155
Haibo Huang329e6812020-05-29 14:12:20 -0700156def delete_branch(proj_path: Path, branch_name: str) -> None:
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800157 """Force delete a branch."""
158 _run(['git', 'branch', '-D', branch_name], cwd=proj_path)
159
160
Haibo Huang329e6812020-05-29 14:12:20 -0700161def start_branch(proj_path: Path, branch_name: str) -> None:
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800162 """Starts a new repo branch."""
163 _run(['repo', 'start', branch_name], cwd=proj_path)
164
165
Haibo Huang329e6812020-05-29 14:12:20 -0700166def commit(proj_path: Path, message: str) -> None:
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800167 """Commits changes."""
168 _run(['git', 'commit', '-m', message], cwd=proj_path)
169
170
Haibo Huang329e6812020-05-29 14:12:20 -0700171def checkout(proj_path: Path, branch_name: str) -> None:
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800172 """Checkouts a branch."""
173 _run(['git', 'checkout', branch_name], cwd=proj_path)
174
175
Joel Galenson58eeae72021-04-07 12:44:31 -0700176def push(proj_path: Path, remote_name: str, has_errors: bool) -> None:
Haibo Huangc3c0cd42019-01-29 15:24:45 -0800177 """Pushes change to remote."""
Haibo Huang806c62b2020-09-25 15:32:36 -0700178 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 Galenson58eeae72021-04-07 12:44:31 -0700183 if has_errors:
184 cmd.extend(['-o', 'l=Verified-1'])
Haibo Huang806c62b2020-09-25 15:32:36 -0700185 _run(cmd, cwd=proj_path)