Skip to content

Commit

Permalink
Add feature to optionally correct branches on sync
Browse files Browse the repository at this point in the history
but only when the repo is clean
  • Loading branch information
gdubicki committed Oct 21, 2022
1 parent 3e70101 commit 07ff6fb
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 27 deletions.
1 change: 1 addition & 0 deletions THANKS
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Tronje Krabbe
Matthew Lovell
Atte Pellikka
Johann Chang
Greg Dubicki


If you make a contribution, feel free to make a pull request to add your name
Expand Down
6 changes: 5 additions & 1 deletion docs/ref/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,12 @@ tsrc status
* Shows dirty repositories
* Shows repositories not on the expected branch

tsrc sync
tsrc sync [--correct-branch/-c]
: Updates all the repositories and shows a summary at the end.
If any of the repositories is not on the configured branch, but it is clean
and the `--correct-branch`/`-c` flag is set, then the branch is changed to
the configured one and then the repository is updated. Otherwise that repository
will not be not updated.

tsrc version
: Displays `tsrc` version number, along additional data if run from a git clone.
Expand Down
3 changes: 2 additions & 1 deletion docs/ref/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Here's the algorithm that is used:
* Run `git fetch --tags --prune`
* Check if the repository is on a branch
* Check if the currently checked out branch matches the one configured in
the manifest
the manifest. If it does not but the `--correct-branch` flag is set
and the repository is clean, the branch is changed to the configured one.
* Check if the repository is dirty
* Try and run a fast-forward merge

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ profile = "black"

[tool.poetry]
name = "tsrc"
version = "2.7.1"
version = "2.8.0pre"
description = "Manage groups of git repositories"
authors = ["Dimitri Merejkowsky <[email protected]>"]
readme = "README.rst"
Expand Down
2 changes: 1 addition & 1 deletion tsrc/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.7.1"
__version__ = "2.8.0pre"
12 changes: 10 additions & 2 deletions tsrc/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def configure_parser(subparser: argparse._SubParsersAction) -> None:
parser = subparser.add_parser("sync")
add_workspace_arg(parser)
add_repos_selection_args(parser)
parser.set_defaults(update_manifest=True, force=False)
parser.set_defaults(update_manifest=True, force=False, correct_branch=False)
parser.add_argument(
"--force", help="use `git fetch --force` while syncing", action="store_true"
)
Expand All @@ -28,6 +28,13 @@ def configure_parser(subparser: argparse._SubParsersAction) -> None:
dest="update_manifest",
help="skip updating the manifest before syncing repositories",
)
parser.add_argument(
"--correct-branch",
"-c",
action="store_true",
dest="correct_branch",
help="go back to the configured branch, if the repo is clean",
)
add_num_jobs_arg(parser)
parser.set_defaults(run=run)

Expand All @@ -39,6 +46,7 @@ def run(args: argparse.Namespace) -> None:
all_cloned = args.all_cloned
regex = args.regex
iregex = args.iregex
correct_branch = args.correct_branch
workspace = get_workspace(args)
num_jobs = get_num_jobs(args)

Expand All @@ -54,6 +62,6 @@ def run(args: argparse.Namespace) -> None:

workspace.clone_missing(num_jobs=num_jobs)
workspace.set_remotes(num_jobs=num_jobs)
workspace.sync(force=force, num_jobs=num_jobs)
workspace.sync(force=force, num_jobs=num_jobs, correct_branch=correct_branch)
workspace.perform_filesystem_operations()
ui.info_1("Workspace synchronized")
74 changes: 55 additions & 19 deletions tsrc/syncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@


class IncorrectBranch(Error):
def __init__(self, *, actual: str, expected: str):
self.message = (
f"Current branch: '{actual}' does not match expected branch: '{expected}'"
)
def __init__(self, *, actual: Optional[str], expected: str):
self.actual = actual
if actual:
self.message = (
f"Current branch: '{actual}' "
f"does not match expected branch: '{expected}'"
)
else:
self.message = f"Not on any branch. Expected branch: '{expected}'"


class Syncer(Task[Repo]):
Expand All @@ -23,10 +28,12 @@ def __init__(
*,
force: bool = False,
remote_name: Optional[str] = None,
correct_branch: bool = False,
) -> None:
self.workspace_path = workspace_path
self.force = force
self.remote_name = remote_name
self.correct_branch = correct_branch

def describe_item(self, item: Repo) -> str:
return item.dest
Expand Down Expand Up @@ -65,7 +72,8 @@ def process(self, index: int, count: int, repo: Repo) -> Outcome:
summary_lines += [repo.dest, "-" * len(repo.dest)]
summary_lines += [f"Reset to {ref}"]
else:
error, current_branch = self.check_branch(repo)
error, current_branch = self.check_or_change_branch(repo)

self.info_3("Updating branch:", current_branch)
sync_summary = self.sync_repo_to_branch(repo, current_branch=current_branch)
if sync_summary:
Expand All @@ -80,34 +88,52 @@ def process(self, index: int, count: int, repo: Repo) -> Outcome:
summary = "\n".join(summary_lines)
return Outcome(error=error, summary=summary)

def check_branch(self, repo: Repo) -> Tuple[Optional[Error], str]:
def check_or_change_branch(self, repo: Repo) -> Tuple[Optional[Error], str]:
"""Check that the current branch:
* exists
* matches the one in the manifest
* Raise Error if the branch does not exist (because we can't
do anything else in that case)
If it does, do nothing.
If not but the repo is clean and the correct_branch flag is set,
switch to the configured branch."""
error = None
current_branch = None

try:
current_branch = self.check_branch(repo)
except IncorrectBranch as e:
current_branch = e.actual

if self.correct_branch:
self.checkout_branch(repo)
current_branch = repo.branch
else:
error = e

* _Return_ on Error if the current branch does not match the
one in the manifest - because we still want to run
`git merge @upstream` in that case
if not current_branch:
raise

* Otherwise, return the current branch
return error, current_branch

def check_branch(self, repo: Repo) -> str:
"""Check that the current branch:
* exists
* matches the one in the manifest
Return the current branch.
"""
repo_path = self.workspace_path / repo.dest
current_branch = None
try:
current_branch = get_current_branch(repo_path)
except Error:
raise Error("Not on any branch")
raise IncorrectBranch(actual=None, expected=repo.branch)

if current_branch and current_branch != repo.branch:
return (
IncorrectBranch(actual=current_branch, expected=repo.branch),
current_branch,
)
else:
return None, current_branch
raise IncorrectBranch(actual=current_branch, expected=repo.branch)

return current_branch

def _pick_remotes(self, repo: Repo) -> List[Remote]:
if self.remote_name:
Expand Down Expand Up @@ -141,6 +167,16 @@ def sync_repo_to_ref(self, repo: Repo, ref: str) -> None:
except Error:
raise Error("updating ref failed")

def checkout_branch(self, repo: Repo) -> None:
repo_path = self.workspace_path / repo.dest
status = get_git_status(repo_path)
if status.dirty:
raise Error(f"git repo is dirty: cannot checkout: {repo.branch}")
try:
self.run_git(repo_path, "checkout", repo.branch)
except Error:
raise Error("checking out failed")

def update_submodules(self, repo: Repo) -> str:
repo_path = self.workspace_path / repo.dest
cmd = ("submodule", "update", "--init", "--recursive")
Expand Down
53 changes: 53 additions & 0 deletions tsrc/test/cli/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,59 @@ def test_changing_branch(
assert message_recorder.find("does not match")


def test_changing_branch_with_correct_branch(
tsrc_cli: CLI,
git_server: GitServer,
workspace_path: Path,
message_recorder: MessageRecorder,
) -> None:
"""Scenario:
* Create a manifest with a foo repo
* Initialize a workspace from this manifest
* Create a new branch named `next` on the foo repo
* Update foo branch in the manifest
* Run `tsrc sync --correct-brach`
* Command succeeds
"""
git_server.add_repo("foo")
manifest_url = git_server.manifest_url
tsrc_cli.run("init", manifest_url)

git_server.push_file("foo", "next.txt", branch="next")
git_server.manifest.set_repo_branch("foo", "next")

tsrc_cli.run("sync", "--correct-branch")


def test_changing_branch_with_correct_branch_but_dirty_repo(
tsrc_cli: CLI,
git_server: GitServer,
workspace_path: Path,
message_recorder: MessageRecorder,
) -> None:
"""Scenario:
* Create a manifest with a foo repo
* Initialize a workspace from this manifest
* Create a new branch named `next` on the foo repo
* Make the repo dirty - create a file but don't push it
* Run `tsrc sync --correct-brach`
* Check that the command fails because the repo
is dirty
"""
git_server.add_repo("foo")
manifest_url = git_server.manifest_url
tsrc_cli.run("init", manifest_url)

git_server.push_file("foo", "next.txt", branch="next")
git_server.manifest.set_repo_branch("foo", "next")

unpushed_file_path = workspace_path / "foo" / "unpushed_file.txt"
unpushed_file_path.touch()

tsrc_cli.run_and_fail("sync", "--correct-branch")
assert message_recorder.find("dirty")


def test_tags_are_not_updated(
tsrc_cli: CLI, git_server: GitServer, workspace_path: Path
) -> None:
Expand Down
9 changes: 7 additions & 2 deletions tsrc/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ def perform_filesystem_operations(
collection.print_errors()
raise FileSystemOperatorError

def sync(self, *, force: bool = False, num_jobs: int = 1) -> None:
def sync(
self, *, force: bool = False, num_jobs: int = 1, correct_branch: bool = False
) -> None:
syncer = Syncer(
self.root_path, force=force, remote_name=self.config.singular_remote
self.root_path,
force=force,
remote_name=self.config.singular_remote,
correct_branch=correct_branch,
)
repos = self.repos
ui.info_2("Synchronizing repos")
Expand Down

0 comments on commit 07ff6fb

Please sign in to comment.