1#!/usr/bin/env python3 2# Copyright 2020 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Git helper functions.""" 7 8import collections 9import os 10from pathlib import Path 11import re 12import subprocess 13import tempfile 14from typing import Iterable, Optional, Union 15 16 17CommitContents = collections.namedtuple("CommitContents", ["url", "cl_number"]) 18 19 20def IsFullGitSHA(s: str) -> bool: 21 """Returns if `s` looks like a git SHA.""" 22 return len(s) == 40 and all(x.isdigit() or "a" <= x <= "f" for x in s) 23 24 25def CreateBranch(repo: Union[Path, str], branch: str) -> None: 26 """Creates a branch in the given repo. 27 28 Args: 29 repo: The absolute path to the repo. 30 branch: The name of the branch to create. 31 32 Raises: 33 ValueError: Failed to create a repo in that directory. 34 """ 35 36 if not os.path.isdir(repo): 37 raise ValueError("Invalid directory path provided: %s" % repo) 38 39 subprocess.check_output(["git", "-C", repo, "reset", "HEAD", "--hard"]) 40 41 subprocess.check_output(["repo", "start", branch], cwd=repo) 42 43 44def DeleteBranch(repo: Union[Path, str], branch: str) -> None: 45 """Deletes a branch in the given repo. 46 47 Args: 48 repo: The absolute path of the repo. 49 branch: The name of the branch to delete. 50 51 Raises: 52 ValueError: Failed to delete the repo in that directory. 53 """ 54 55 if not os.path.isdir(repo): 56 raise ValueError("Invalid directory path provided: %s" % repo) 57 58 def run_checked(cmd): 59 subprocess.run(["git", "-C", repo] + cmd, check=True) 60 61 run_checked(["checkout", "-q", "m/main"]) 62 run_checked(["reset", "-q", "HEAD", "--hard"]) 63 run_checked(["branch", "-q", "-D", branch]) 64 65 66def CommitChanges( 67 repo: Union[Path, str], commit_messages: Iterable[str] 68) -> None: 69 """Commit changes without uploading them. 70 71 Args: 72 repo: The absolute path to the repo where changes were made. 73 commit_messages: Messages to concatenate to form the commit message. 74 """ 75 if not os.path.isdir(repo): 76 raise ValueError("Invalid path provided: %s" % repo) 77 78 # Create a git commit. 79 with tempfile.NamedTemporaryFile(mode="w+t", encoding="utf-8") as f: 80 f.write("\n".join(commit_messages)) 81 f.flush() 82 83 subprocess.check_output(["git", "commit", "-F", f.name], cwd=repo) 84 85 86def UploadChanges( 87 repo: Union[Path, str], 88 branch: str, 89 reviewers: Optional[Iterable[str]] = None, 90 cc: Optional[Iterable[str]] = None, 91 wip: bool = False, 92) -> CommitContents: 93 """Uploads the changes in the specifed branch of the given repo for review. 94 95 Args: 96 repo: The absolute path to the repo where changes were made. 97 branch: The name of the branch to upload. 98 of the changes made. 99 reviewers: A list of reviewers to add to the CL. 100 cc: A list of contributors to CC about the CL. 101 wip: Whether to upload the change as a work-in-progress. 102 103 Returns: 104 A CommitContents value containing the commit URL and change list number. 105 106 Raises: 107 ValueError: Failed to create a commit or failed to upload the 108 changes for review. 109 """ 110 111 if not os.path.isdir(repo): 112 raise ValueError("Invalid path provided: %s" % repo) 113 114 # Upload the changes for review. 115 git_args = [ 116 "repo", 117 "upload", 118 "--yes", 119 f'--reviewers={",".join(reviewers)}' if reviewers else "--ne", 120 "--no-verify", 121 f"--br={branch}", 122 ] 123 124 if cc: 125 git_args.append(f'--cc={",".join(cc)}') 126 if wip: 127 git_args.append("--wip") 128 129 out = subprocess.check_output( 130 git_args, 131 stderr=subprocess.STDOUT, 132 cwd=repo, 133 encoding="utf-8", 134 ) 135 136 print(out) 137 # Matches both internal and external CLs. 138 found_url = re.search( 139 r"https?://[\w-]*-review.googlesource.com/c/.*/([0-9]+)", 140 out.rstrip(), 141 ) 142 if not found_url: 143 raise ValueError("Failed to find change list URL.") 144 145 return CommitContents( 146 url=found_url.group(0), cl_number=int(found_url.group(1)) 147 ) 148