def check_branch_version( current_branch: str, kind: BranchKind, next_version: version.Version, prev_version: Optional[version.Version] ): if prev_version and kind == BranchKind.RELEASE and prev_version.release != next_version.release: raise ValueError("The semantic version cannot be changed on a release branch.") expected_release_branch_name = "release/v" + ".".join(str(i) for i in next_version.release) if kind == BranchKind.RELEASE and current_branch != expected_release_branch_name: raise StateError( "The semantic version does not match the release branch name: " f"{expected_release_branch_name} != {current_branch}" ) if prev_version and kind != BranchKind.VARIANT and prev_version.local: raise StateError( f"The non-variant branch {current_branch} has a variant. This should not happen. Somebody broke the rules." ) if kind != BranchKind.VARIANT and next_version.local: raise ValueError(f"The variant cannot be set on the non-variant branch {current_branch}.") if kind == BranchKind.VARIANT and next_version.local != current_branch.split("/", maxsplit=1)[1]: branch_variant = current_branch.split("/", maxsplit=1)[1] raise StateError( "The variant in the version and the variant in the branch name must be the same: " f"{next_version.local} != {branch_variant}" ) if prev_version and next_version <= prev_version: raise ValueError(f"The next version ({next_version}) must be greater than the current one ({prev_version})") if prev_version and kind == BranchKind.VARIANT and next_version.release != prev_version.release: raise ValueError( "To change the version of a variant branch, merge master into the variant branch. " "Bumping the version directly on the variant branch is not allowed." )
def check_clean(args, config): if not config.open: raise StateError("Action cannot run in a source tree that is not a git clone.") is_dirty = git.is_dirty(config.open) or (config.has_enterprise and git.is_dirty(config.enterprise)) if not args.clean and is_dirty: raise StateError("Action only supported in clean repositories. (Stash your changes.)")
def check_at_branch(branch, config): check_remotes(config) if git.get_hash(f"{config.open.upstream_remote}/{branch}", config.open) != git.get_hash(git.HEAD, config.open): raise StateError(f"{config.open.dir} HEAD is up to date with {branch}") if config.has_enterprise and git.get_hash( f"{config.enterprise.upstream_remote}/{branch}", config.enterprise ) != git.get_hash(git.HEAD, config.enterprise): raise StateError(f"{config.enterprise.dir} HEAD is up to date with {branch}")
def get_current_branch_from_either_repository(config: Configuration): current_branch = git.get_branch_checked_out(config.open, ref_only=True) if config.has_enterprise: current_branch = current_branch or git.get_branch_checked_out(config.enterprise, ref_only=True) if not current_branch: raise StateError("Operation is not supported without a branch checked out (currently HEAD is detached).") return current_branch
def tag_subcommand(args): config: Configuration = args.configuration check_remotes(config) check_clean(args, config) commit = git.HEAD current_branch = get_current_branch_from_either_repository(config) kind = get_branch_kind(current_branch, (BranchKind.RELEASE, BranchKind.VARIANT)) if ( not git.is_ancestor_of(commit, f"{config.open.upstream_remote}/{current_branch}", config.open) and args.require_upstream and not args.pretend_upstream ): raise StateError(f"HEAD of {current_branch} is not upstream") if ( not git.is_ancestor_of(commit, f"{config.enterprise.upstream_remote}/{current_branch}", config.enterprise) and args.require_upstream and not args.pretend_upstream ): raise StateError(f"HEAD of {current_branch} is not upstream") next_version = version.Version(args.version) check_branch_version(current_branch, kind, next_version, prev_version=None) tag_name = f"v{format_version_pep440(next_version)}" title = f"Version {format_version_pep440(next_version)}" g = GithubFacade(config) def tag_repo(repo: Repo): if git.is_ancestor_of(commit, f"{repo.upstream_remote}/{current_branch}", repo) or args.pretend_upstream: g.create_tag(repo.upstream_url, git.get_hash(commit, repo, pretend_clean=True), tag_name, message=title) else: raise NotImplementedError("To tag a release commit, the commit must already be in the upstream branch") tag_repo(config.open) if config.has_enterprise: tag_repo(config.enterprise) fetch_upstream(config) warn_dry_run(args)
def update_dependent_pr_subcommand(args): config: Configuration = args.configuration check_remotes(config) check_clean(args, config) g = GithubFacade(config) enterprise_pr = g.get_pr(config.enterprise.upstream_url, number=args.number) if enterprise_pr.commits > 1: raise NotImplementedError( "update_dependent_pr only supports single commit PRs. (It could be implemented if needed.)" ) after_match = PR_AFTER_RE.search(enterprise_pr.body) if not after_match: raise ValueError( f"PR {enterprise_pr.base.repo.full_name}#{enterprise_pr.number} does not have an 'After:' annotation." ) if after_match.group("external_number"): repo_full_name = "{username}/{repository}".format(**after_match.groupdict()) open_repo = g.github.get_repo(repo_full_name) open_pr = open_repo.get_pull(int(after_match.group("external_number"))) if not open_pr.merged: raise StateError(f"The dependency {open_repo.full_name}#{open_pr.number} is not merged.") enterprise_original_branch = git.get_branch_checked_out(config.enterprise) open_original_branch = git.get_branch_checked_out(config.open) git.switch(enterprise_pr.head.ref, config.enterprise, config.dry_run) git.switch(open_pr.merge_commit_sha, config.open, config.dry_run) git.commit_amend([SUBMODULE_PATH], config.enterprise, config.dry_run) git.push(config.enterprise.origin_remote, enterprise_pr.head.ref, config.enterprise, config.dry_run, force=True) git.switch(enterprise_original_branch, config.enterprise, config.dry_run) git.switch(open_original_branch, config.open, config.dry_run) warn_dry_run(args) return [f"TODO: Merge {enterprise_pr.html_url} as soon as possible."] raise StateError( "PR does not have an acceptable 'After:' annotation. Only external PR references are supported. " f"(Was '{after_match.group(0)}')" )
def release_branch_subcommand(args): config: Configuration = args.configuration check_clean(args, config) if not args.allow_arbitrary_branch: current_branch = get_current_branch_from_either_repository(config) get_branch_kind(current_branch, [BranchKind.MASTER]) check_at_branch("master", config) if config.has_enterprise: git.switch("master", config.enterprise, config.dry_run) else: # Do not switch branches in open if we did so in enterprise. This allows external/katana to lag # at branch time. git.switch("master", config.open, config.dry_run) else: print( "WARNING: Branching from HEAD instead of upstream/master. Be careful! This will create an out of date " "release branch." ) # Check if HEAD is on the upstream master branch. if ( not git.is_ancestor_of(git.HEAD, f"{config.open.upstream_remote}/master", config.open) and not args.pretend_upstream ): raise StateError(f"{config.open.dir} HEAD is not on upstream master") if ( config.has_enterprise and not git.is_ancestor_of(git.HEAD, f"{config.enterprise.upstream_remote}/master", config.enterprise) and not args.pretend_upstream ): raise StateError(f"{config.enterprise.dir} HEAD is not on upstream master") prev_version, _ = get_explicit_version(git.HEAD, True, config.open, config.version_file, no_dev=True) next_version = version.Version(args.next_version) rc_version = version.Version(f"{prev_version}rc1") # Always pretend we are on master. We either actually are, or the user has overridden things. check_branch_version("master", BranchKind.MASTER, next_version, prev_version) g = GithubFacade(config) # Create release branches. release_branch_name = f"release/v{format_version_pep440(prev_version)}" check_branch_version(release_branch_name, BranchKind.RELEASE, rc_version, add_dev_to_version(prev_version)) g.create_branch( config.open.upstream_url, git.get_hash(git.HEAD, config.open, pretend_clean=True), release_branch_name, ) if config.has_enterprise: g.create_branch( config.enterprise.upstream_url, git.get_hash(git.HEAD, config.enterprise, pretend_clean=True), release_branch_name, ) # Create a PR on master which updates the version.txt to {next version}. todos = bump_both_repos(config, g, prev_version, next_version, "master") # Create a PR on the release branch which updates the version.txt to {version}rc1. todos.extend(bump_both_repos(config, g, prev_version, rc_version, release_branch_name)) warn_dry_run(args) return todos
def check_remote(repo, remote, remote_name): if not remote: raise StateError(f"{repo}: Repository must have an {remote_name} remote. Your work flow is not supported.")
def check_branch_not_exist(config: Configuration, branch_name): check_remotes(config) if git.ref_exists(branch_name, config.open): raise StateError(f"Branch {branch_name} already exists in {config.open.dir.name}") if git.ref_exists(branch_name, config.enterprise): raise StateError(f"Branch {branch_name} already exists in {config.enterprise.dir.name}")
def get_branch_kind(current_branch, kinds: Iterable[BranchKind]): for kind in kinds: if re.match(kind.value, current_branch): return kind kinds_str = ", ".join(k.value for k in kinds) raise StateError(f"The current branch ({current_branch}) should be one of: {kinds_str}")