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