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