#!/usr/bin/env python import logging import os import re import sys import argparse from git import Repo FORMAT = '[%(levelname)s] %(message)s' logging.basicConfig(format=FORMAT, level=logging.DEBUG) remote_name = "upstream" remote_master_branch = "{}/master".format(remote_name) class VersionParser: """A parser for version numbers""" def __init__(self, versionString): # Validate that user passed a valid version VERSION_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$") match = VERSION_RE.match(versionString) if not match: raise ValueError("Invalid version (should be X.Y.Z)") # Parse the version component and build proper version strings self.major = int(match.group(1)) self.minor = int(match.group(2)) self.patch = int(match.group(3)) self.version = "v{}.{}.{}".format(self.major, self.minor, self.patch) # clean up version self.is_major_release = False self.is_minor_release = False self.is_patch_release = False if self.patch != 0: self.is_patch_release = True self.previous_version = "v{}.{}.{}".format(self.major, self.minor, self.patch - 1) elif self.minor != 0: self.is_minor_release = True self.previous_version = "v{}.{}.{}".format(self.major, self.minor - 1, 0) else: self.is_major_release = True self.previous_version = "v{}.{}.{}".format(self.major - 1, 0, 0) raise ValueError("Major releases not yet supported") # Build the branch names self.previous_rc_branch = "{}-rc".format(self.previous_version) self.base_branch = "{}-rc-base".format(self.version) self.rc_branch = "{}-rc".format(self.version) self.remote_previous_rc_branch = "{}/{}".format(remote_name, self.previous_rc_branch) self.remote_base_branch = "{}/{}".format(remote_name, self.base_branch) self.remote_rc_branch = "{}/{}".format(remote_name, self.rc_branch) def checkVersionBranches(version): """Check that the branches for a given version were created properly.""" parser = VersionParser(version) repo = Repo(os.getcwd(), search_parent_directories=True) assert not repo.bare # Verify the branches' existance if parser.remote_previous_rc_branch not in repo.refs: raise ValueError("Previous RC branch not found: {}".format(parser.remote_previous_rc_branch)) if parser.remote_base_branch not in repo.refs: raise ValueError("Base branch not found: {}".format(parser.remote_base_branch)) if parser.remote_rc_branch not in repo.refs: raise ValueError("RC branch not found: {}".format(parser.remote_rc_branch)) previous_rc = repo.refs[parser.remote_previous_rc_branch] current_rc_base = repo.refs[parser.remote_base_branch] current_rc = repo.refs[parser.remote_rc_branch] master = repo.refs[remote_master_branch] # Check the base branch is an ancestor of the rc branch if not repo.is_ancestor(current_rc_base, current_rc): raise ValueError("{} is not an ancesctor of {}".format(current_rc_base, current_rc)) # Check that the base branch is the merge base of the previous and current RCs merge_base = repo.merge_base(previous_rc, current_rc) if current_rc_base.commit not in merge_base: raise ValueError("Base branch is not the merge base between {} and {}".format(previous_rc, current_rc)) # For patch releases, warn if the base commit is not the previous RC commit if parser.is_patch_release: if parser.previous_version not in repo.tags: logging.warning("The tag {0} does not exist, which suggests {0} has not been released.".format(parser.previous_version)) if current_rc_base.commit != previous_rc.commit: logging.warning("Previous version has commits not in this patch"); logging.warning("Type \"git diff {}..{}\" to see the commit list".format(current_rc_base, previous_rc)); # Check base branch is part of the previous RC previous_rc_base_commit = repo.merge_base(previous_rc, master) if repo.is_ancestor(current_rc_base, previous_rc_base_commit): raise ValueError("{} is older than {}".format(current_rc_base, previous_rc)) print("[SUCCESS] Checked {}".format(parser.version)) def createVersionBranches(version): """Create the branches for a given version.""" parser = VersionParser(version) repo = Repo(os.getcwd(), search_parent_directories=True) assert not repo.bare # Validate the user is on a local branch that has the right merge base if repo.head.is_detached: raise ValueError("You must not run this script in a detached HEAD state") # Validate the user has no pending changes if repo.is_dirty(): raise ValueError("Your working tree has pending changes. You must have a clean working tree before proceeding.") # Make sure the remote is up to date remote = repo.remotes[remote_name] remote.fetch(prune=True) # Verify the previous RC branch exists if parser.remote_previous_rc_branch not in repo.refs: raise ValueError("Previous RC branch not found: {}".format(parser.remote_previous_rc_branch)) # Verify the branches don't already exist if parser.remote_base_branch in repo.refs: raise ValueError("Base branch already exists: {}".format(parser.remote_base_branch)) if parser.remote_rc_branch in repo.refs: raise ValueError("RC branch already exists: {}".format(parser.remote_rc_branch)) if parser.base_branch in repo.refs: raise ValueError("Base branch already exists locally: {}".format(parser.base_branch)) if parser.rc_branch in repo.refs: raise ValueError("RC branch already exists locally: {}".format(parser.rc_branch)) # Save current branch name current_branch_name = repo.active_branch # Create the RC branches if parser.is_patch_release: # Check tag exists, if it doesn't, print warning and ask for comfirmation if parser.previous_version not in repo.tags: logging.warning("The tag {0} does not exist, which suggests {0} has not yet been released.".format(parser.previous_version)) logging.warning("Creating the branches now means that {0} will diverge from {1} if anything is merged into {1}.".format(parser.version, parser.previous_version)) logging.warning("This is not recommended unless necessary.") validAnswer = False askCount = 0 while not validAnswer and askCount < 3: answer = input("Are you sure you want to do this? [y/n] ").strip().lower() askCount += 1 validAnswer = answer == "y" or answer == "n" if not validAnswer: raise ValueError("Did not understand response") if answer == "n": print("Aborting") return else: print("Creating branches") previous_rc = repo.refs[parser.remote_previous_rc_branch] repo.create_head(parser.base_branch, previous_rc) remote.push("{0}:{0}".format(parser.base_branch)) repo.create_head(parser.rc_branch, previous_rc) remote.push("{0}:{0}".format(parser.rc_branch)) else: previous_rc = repo.refs[parser.remote_previous_rc_branch] master = repo.refs[remote_master_branch] merge_base = repo.merge_base(previous_rc, master) repo.create_head(parser.base_branch, merge_base[0]) remote.push("{0}:{0}".format(parser.base_branch)) repo.create_head(parser.rc_branch, master) remote.push("{0}:{0}".format(parser.rc_branch)) print("[SUCCESS] Created {} and {}".format(parser.base_branch, parser.rc_branch)) print("[SUCCESS] You can make the PR from the following webpage:") print("[SUCCESS] https://github.com/highfidelity/hifi/compare/{}...{}".format(parser.base_branch, parser.rc_branch)) if parser.is_patch_release: print("[SUCCESS] NOTE: You will have to wait for the first fix to be merged into the RC branch to be able to create the PR") def main(): """Execute Main entry point.""" global remote_name parser = argparse.ArgumentParser(description='RC branches tool', formatter_class=argparse.RawDescriptionHelpFormatter, epilog='''Example commands you can run:\n%(prog)s check 0.75.0\n%(prog)s create 0.75.1''') parser.add_argument("command", help="command to execute", choices=["check", "create"]) parser.add_argument("version", help="version of the form \"X.Y.Z\"") parser.add_argument("--remote", dest="remote_name", default=remote_name, help="git remote to use as reference") args = parser.parse_args() remote_name = args.remote_name try: if args.command == "check": checkVersionBranches(args.version) elif args.command == "create": createVersionBranches(args.version) else: parser.print_help() except ValueError as ex: logging.error(ex) sys.exit(1) if __name__ == "__main__": main()