-
Notifications
You must be signed in to change notification settings - Fork 1
BWDO-778 sc branch rename, rm_merged and more #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| from dataclasses import dataclass | ||
| from enum import Enum, auto | ||
| import logging | ||
| from pathlib import Path | ||
|
|
||
| import git | ||
| from git import Repo | ||
|
|
||
| from .command import Command | ||
| from sc.exceptions import ScError | ||
| from sc_manifest_parser import ScManifest | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| class BranchState(Enum): | ||
| RENAME_NEEDED = auto() | ||
| ALREADY_RENAMED = auto() | ||
| INVALID = auto() | ||
|
|
||
| @dataclass | ||
| class RemoteInfo: | ||
| name: str | ||
| old_commit: str | None | ||
| new_commit: str | None | ||
|
|
||
| @dataclass | ||
| class BranchRename(Command): | ||
| old_branch: str | ||
| new_branch: str | ||
|
|
||
| def run_git_command(self): | ||
| self._rename_branch(self.top_dir, self.old_branch, self.new_branch) | ||
|
|
||
| def run_repo_command(self): | ||
| manifest = ScManifest.from_repo_root(self.top_dir / ".repo") | ||
|
|
||
| for proj in manifest.projects: | ||
| if proj.lock_status is None: | ||
| logger.info(f"Attempting renaming branch in repo: {self.top_dir / proj.path}") | ||
| self._rename_branch( | ||
| self.top_dir / proj.path, | ||
| self.old_branch, | ||
| self.new_branch, | ||
| proj.remote, | ||
| ) | ||
|
|
||
| logger.info(f"Attempting renaming manifest branch: {self.top_dir / '.repo' / 'manifests'}") | ||
| self._rename_branch( | ||
| self.top_dir / ".repo" / "manifests", | ||
| self.old_branch, | ||
| self.new_branch, | ||
| ) | ||
|
|
||
| def _rename_branch( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd break this into 2 functions. One for the local branch rename and one of the remote branch rename. We flip between local and remote commands here and it's get confusing.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I split out the remote checking into its own func which should be less confusing hopefully. The reason I didn't have 2 separate functions is it has a check for a remote branch already existing in the name you specified and then it won't let you rename the local. |
||
| self, | ||
| directory: Path, | ||
| old_branch: str, | ||
| new_branch: str, | ||
| remote_name: str | None = None): | ||
| try: | ||
| repo = Repo(directory) | ||
| except git.InvalidGitRepositoryError as e: | ||
| raise ScError(f"Invalid git repo: {directory}") from e | ||
|
|
||
| branch_state = self._get_rename_state(repo, old_branch, new_branch) | ||
| if branch_state is BranchState.INVALID: | ||
| return | ||
|
|
||
| try: | ||
| remote = self._get_remote_name(repo, remote_name) | ||
| except RuntimeError as e: | ||
| logger.warning(f"{e} Skipping remote renaming.") | ||
| remote = None | ||
|
|
||
| # Need to check here so not to rename locally if a different branch already exists | ||
| # on the remote with the new name. | ||
| if remote and self._has_remote_conflict(repo, remote, old_branch, new_branch, branch_state): | ||
| return | ||
|
|
||
| if branch_state is BranchState.RENAME_NEEDED: | ||
| self._rename_local(repo, old_branch, new_branch) | ||
|
|
||
| if remote: | ||
| self._rename_remote( | ||
| repo, | ||
| remote, | ||
| old_branch, | ||
| new_branch, | ||
| ) | ||
|
|
||
| def _get_rename_state(self, repo: Repo, old_branch: str, new_branch: str) -> BranchState: | ||
| has_old = self._has_branch(repo, old_branch) | ||
| has_new = self._has_branch(repo, new_branch) | ||
|
|
||
| if has_old and not has_new: | ||
| return BranchState.RENAME_NEEDED | ||
|
|
||
| if not has_old and has_new: | ||
| logger.info(f"Branch already renamed in local repo {repo.working_dir}.") | ||
| return BranchState.ALREADY_RENAMED | ||
|
|
||
| if has_old and has_new: | ||
| logger.warning( | ||
| f"Both branches exist in repo {repo.working_dir}: " | ||
| f"{old_branch}, {new_branch}. Skipping." | ||
| ) | ||
| return BranchState.INVALID | ||
|
|
||
| logger.warning( | ||
| f"Neither branch exists in repo {repo.working_dir}: " | ||
| f"{old_branch}, {new_branch}. Skipping." | ||
| ) | ||
| return BranchState.INVALID | ||
|
|
||
| def _rename_local(self, repo: Repo, old_branch: str, new_branch: str): | ||
| try: | ||
| repo.git.branch("-m", old_branch, new_branch) | ||
| logger.info("Renamed locally.") | ||
| except git.GitCommandError as e: | ||
| raise ScError( | ||
| f"Unable to rename branch in repo {repo.working_dir}: {e.stderr}" | ||
| ) from e | ||
|
|
||
| def _rename_remote( | ||
| self, | ||
| repo: Repo, | ||
| remote: str, | ||
| old_branch: str, | ||
| new_branch: str, | ||
| ): | ||
| try: | ||
| # Push/create the new branch before deleting the old branch. | ||
| repo.git.push("-u", remote, f"{new_branch}:refs/heads/{new_branch}") | ||
|
|
||
| if self._remote_branch_commit(repo, remote, old_branch) is not None: | ||
| repo.git.push(remote, "--delete", old_branch) | ||
|
|
||
| logger.info("Renamed remotely.") | ||
| except git.GitCommandError as e: | ||
| logger.warning( | ||
| f"Failed to rename branch in remote in repo {repo.working_dir}: " | ||
| f"{e.stderr}" | ||
| ) | ||
|
|
||
| def _has_remote_conflict( | ||
| self, | ||
| repo: Repo, | ||
| remote: str, | ||
| old_branch: str, | ||
| new_branch: str, | ||
| branch_state: BranchState) -> bool: | ||
| """ | ||
| True if new branch already exists on remote and isn't the same commit | ||
| as the branch we are renaming. | ||
| """ | ||
| try: | ||
| new_remote_commit = self._remote_branch_commit(repo, remote, new_branch) | ||
| except git.GitCommandError as e: | ||
| logger.warning( | ||
| f"Unable to query remote {remote} in repo {repo.working_dir}. " | ||
| f"Skipping remote branch rename: {e.stderr}" | ||
| ) | ||
| return True | ||
|
|
||
| if new_remote_commit is None: | ||
| # New branch doesn't exist on remote. | ||
| return False | ||
|
|
||
| local_branch = old_branch | ||
| if branch_state is BranchState.ALREADY_RENAMED: | ||
| local_branch = new_branch | ||
|
|
||
| local_commit = self._local_branch_commit(repo, local_branch) | ||
| if local_commit == new_remote_commit: | ||
| # Already renamed on remote. | ||
| return False | ||
|
|
||
| logger.warning( | ||
| f"Remote branch {new_branch} already exists on {remote} in repo " | ||
| f"{repo.working_dir} and points at a different commit. Skipping." | ||
| ) | ||
| return True | ||
|
|
||
| def _get_remote_name(self, repo: Repo, remote_name: str | None) -> str: | ||
| if remote_name: | ||
| try: | ||
| _ = repo.remote(remote_name) | ||
| return remote_name | ||
| except ValueError as e: | ||
| raise RuntimeError( | ||
| f"No remote named {remote_name} found for {repo.working_dir}.") from e | ||
|
|
||
| try: | ||
| return repo.remotes[0].name | ||
| except IndexError as e: | ||
| raise RuntimeError(f"No remote found for repo {repo.working_dir}.") from e | ||
|
|
||
| def _has_branch(self, repo: Repo, branch_name: str) -> bool: | ||
| return branch_name in [branch.name for branch in repo.branches] | ||
|
|
||
| def _local_branch_commit(self, repo: Repo, branch_name: str) -> str: | ||
| return repo.git.rev_parse(branch_name) | ||
|
|
||
| def _remote_branch_commit( | ||
| self, | ||
| repo: Repo, | ||
| remote_name: str, | ||
| branch_name: str) -> str | None: | ||
| out = repo.git.ls_remote("--heads", remote_name, f"refs/heads/{branch_name}") | ||
|
|
||
| for line in out.splitlines(): | ||
| commit, ref = line.split() | ||
| if ref == f"refs/heads/{branch_name}": | ||
| return commit | ||
|
Comment on lines
+204
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the usage of this, surely it should just be a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's also used to check if there exists a remote branch with the new name which is the same commit as the local branch we're renaming. Which is pretty unlikely but allows it to go through in this situation. |
||
|
|
||
| return None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstrings would be nice in this one.