From b183590096e0cf189e5fa636b32f02d1d858bb7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Thu, 23 Dec 2021 18:33:14 +0100 Subject: [PATCH] Add default tracking configuration --- docs/src/worktrees.md | 30 ++++ e2e_tests/test_worktrees.py | 274 +++++++++++++++++++++++++++++++++--- src/grm/cmd.rs | 3 + src/grm/main.rs | 1 + src/lib.rs | 112 +++++++++++---- src/repo.rs | 10 ++ 6 files changed, 382 insertions(+), 48 deletions(-) diff --git a/docs/src/worktrees.md b/docs/src/worktrees.md index 8feb16f..7dd4799 100644 --- a/docs/src/worktrees.md +++ b/docs/src/worktrees.md @@ -134,6 +134,36 @@ The behaviour of `--track` differs depending on the existence of the remote bran new remote tracking branch, using the default branch (either `main` or `master`) as the base +Often, you'll have a workflow that uses tracking branches by default. It would +be quite tedious to add `--track` every single time. Luckily, the `grm.toml` file +supports defaults for the tracking behaviour. See this for an example: + +```toml +[track] +default = true +default_remote = "origin" +``` + +This will set up a tracking branch on `origin` that has the same name as the local +branch. + +Sometimes, you might want to have a certain prefix for all your tracking branches. +Maybe to prevent collissions with other contributors. You can simply set +`default_remote_prefix` in `grm.toml`: + +```toml +[track] +default = true +default_remote = "origin" +default_remote_prefix = "myname" +``` + +When using branch `my-feature-branch`, the remote tracking branch would be +`origin/myname/my-feature-branch` in this case. + +Note that `--track` overrides any configuration in `grm.toml`. If you want to +disable tracking, use `--no-track`. + ### Showing the status of your worktrees There is a handy little command that will show your an overview over all worktrees diff --git a/e2e_tests/test_worktrees.py b/e2e_tests/test_worktrees.py index 4a63cd0..7539641 100644 --- a/e2e_tests/test_worktrees.py +++ b/e2e_tests/test_worktrees.py @@ -4,38 +4,266 @@ from helpers import * import git +import os.path + def test_worktree_add_simple(): - with TempGitRepositoryWorktree() as base_dir: - cmd = grm(["wt", "add", "test"], cwd=base_dir) - assert cmd.returncode == 0 + for remote_branch_already_exists in (True, False): + for has_config in (True, False): + for has_default in (True, False): + for has_prefix in (True, False): + with TempGitRepositoryWorktree() as base_dir: + if has_config: + with open(os.path.join(base_dir, "grm.toml"), "w") as f: + f.write( + f""" + [track] + default = {str(has_default).lower()} + default_remote = "origin" + """ + ) + if has_prefix: + f.write( + """ + default_remote_prefix = "myprefix" + """ + ) - files = os.listdir(base_dir) - assert len(files) == 2 - assert set(files) == {".git-main-working-tree", "test"} + if remote_branch_already_exists: + shell( + f""" + cd {base_dir} + git --git-dir ./.git-main-working-tree worktree add tmp + ( + cd tmp + touch change + git add change + git commit -m commit + git push origin HEAD:test + #git reset --hard 'HEAD@{1}' + git branch -va + ) + git --git-dir ./.git-main-working-tree worktree remove tmp + """ + ) + cmd = grm(["wt", "add", "test"], cwd=base_dir) + assert cmd.returncode == 0 - repo = git.Repo(os.path.join(base_dir, "test")) - assert not repo.bare - assert not repo.is_dirty() - assert str(repo.active_branch) == "test" - assert repo.active_branch.tracking_branch() is None + files = os.listdir(base_dir) + if has_config is True: + assert len(files) == 3 + assert set(files) == { + ".git-main-working-tree", + "grm.toml", + "test", + } + else: + assert len(files) == 2 + assert set(files) == {".git-main-working-tree", "test"} + + repo = git.Repo(os.path.join(base_dir, "test")) + assert not repo.bare + assert not repo.is_dirty() + if has_config and has_default: + if has_prefix and not remote_branch_already_exists: + assert ( + str(repo.active_branch.tracking_branch()) + == "origin/myprefix/test" + ) + else: + assert ( + str(repo.active_branch.tracking_branch()) + == "origin/test" + ) + else: + assert repo.active_branch.tracking_branch() is None def test_worktree_add_with_tracking(): - with TempGitRepositoryWorktree() as base_dir: - cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir) - print(cmd.stderr) - assert cmd.returncode == 0 + for remote_branch_already_exists in (True, False): + for has_config in (True, False): + for has_default in (True, False): + for has_prefix in (True, False): + with TempGitRepositoryWorktree() as base_dir: + if has_config: + with open(os.path.join(base_dir, "grm.toml"), "w") as f: + f.write( + f""" + [track] + default = {str(has_default).lower()} + default_remote = "origin" + """ + ) + if has_prefix: + f.write( + """ + default_remote_prefix = "myprefix" + """ + ) - files = os.listdir(base_dir) - assert len(files) == 2 - assert set(files) == {".git-main-working-tree", "test"} + if remote_branch_already_exists: + shell( + f""" + cd {base_dir} + git --git-dir ./.git-main-working-tree worktree add tmp + ( + cd tmp + touch change + git add change + git commit -m commit + git push origin HEAD:test + #git reset --hard 'HEAD@{1}' + git branch -va + ) + git --git-dir ./.git-main-working-tree worktree remove tmp + """ + ) + cmd = grm( + ["wt", "add", "test", "--track", "origin/test"], + cwd=base_dir, + ) + print(cmd.stderr) + assert cmd.returncode == 0 - repo = git.Repo(os.path.join(base_dir, "test")) - assert not repo.bare - assert not repo.is_dirty() - assert str(repo.active_branch) == "test" - assert str(repo.active_branch.tracking_branch()) == "origin/test" + files = os.listdir(base_dir) + if has_config is True: + assert len(files) == 3 + assert set(files) == { + ".git-main-working-tree", + "grm.toml", + "test", + } + else: + assert len(files) == 2 + assert set(files) == {".git-main-working-tree", "test"} + + repo = git.Repo(os.path.join(base_dir, "test")) + assert not repo.bare + assert not repo.is_dirty() + assert str(repo.active_branch) == "test" + assert ( + str(repo.active_branch.tracking_branch()) == "origin/test" + ) + + +def test_worktree_add_with_explicit_no_tracking(): + for has_config in (True, False): + for has_default in (True, False): + for has_prefix in (True, False): + for track in (True, False): + with TempGitRepositoryWorktree() as base_dir: + if has_config: + with open(os.path.join(base_dir, "grm.toml"), "w") as f: + f.write( + f""" + [track] + default = {str(has_default).lower()} + default_remote = "origin" + """ + ) + if has_prefix: + f.write( + """ + default_remote_prefix = "myprefix" + """ + ) + if track is True: + cmd = grm( + [ + "wt", + "add", + "test", + "--track", + "origin/test", + "--no-track", + ], + cwd=base_dir, + ) + else: + cmd = grm(["wt", "add", "test", "--no-track"], cwd=base_dir) + print(cmd.stderr) + assert cmd.returncode == 0 + + files = os.listdir(base_dir) + if has_config is True: + assert len(files) == 3 + assert set(files) == { + ".git-main-working-tree", + "grm.toml", + "test", + } + else: + assert len(files) == 2 + assert set(files) == {".git-main-working-tree", "test"} + + repo = git.Repo(os.path.join(base_dir, "test")) + assert not repo.bare + assert not repo.is_dirty() + assert str(repo.active_branch) == "test" + assert repo.active_branch.tracking_branch() is None + + +def test_worktree_add_with_config(): + for remote_branch_already_exists in (True, False): + for has_default in (True, False): + for has_prefix in (True, False): + with TempGitRepositoryWorktree() as base_dir: + with open(os.path.join(base_dir, "grm.toml"), "w") as f: + f.write( + f""" + [track] + default = {str(has_default).lower()} + default_remote = "origin" + """ + ) + if has_prefix: + f.write( + """ + default_remote_prefix = "myprefix" + """ + ) + if remote_branch_already_exists: + shell( + f""" + cd {base_dir} + git --git-dir ./.git-main-working-tree worktree add tmp + ( + cd tmp + touch change + git add change + git commit -m commit + git push origin HEAD:test + #git reset --hard 'HEAD@{1}' + git branch -va + ) + git --git-dir ./.git-main-working-tree worktree remove tmp + """ + ) + cmd = grm(["wt", "add", "test"], cwd=base_dir) + print(cmd.stderr) + assert cmd.returncode == 0 + + files = os.listdir(base_dir) + assert len(files) == 3 + assert set(files) == {".git-main-working-tree", "grm.toml", "test"} + + repo = git.Repo(os.path.join(base_dir, "test")) + assert not repo.bare + assert not repo.is_dirty() + assert str(repo.active_branch) == "test" + if has_default: + if has_prefix and not remote_branch_already_exists: + assert ( + str(repo.active_branch.tracking_branch()) + == "origin/myprefix/test" + ) + else: + assert ( + str(repo.active_branch.tracking_branch()) + == "origin/test" + ) + else: + assert repo.active_branch.tracking_branch() is None def test_worktree_delete(): diff --git a/src/grm/cmd.rs b/src/grm/cmd.rs index 6cf4a18..4532427 100644 --- a/src/grm/cmd.rs +++ b/src/grm/cmd.rs @@ -102,6 +102,9 @@ pub struct WorktreeAddArgs { pub branch_namespace: Option, #[clap(short = 't', long = "track", about = "Remote branch to track")] pub track: Option, + + #[clap(long = "--no-track", about = "Disable tracking")] + pub no_track: bool, } #[derive(Parser)] pub struct WorktreeDeleteArgs { diff --git a/src/grm/main.rs b/src/grm/main.rs index b6dad4d..8c2e8cd 100644 --- a/src/grm/main.rs +++ b/src/grm/main.rs @@ -162,6 +162,7 @@ fn main() { &action_args.name, action_args.branch_namespace.as_deref(), track, + action_args.no_track, ) { Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)), Err(error) => { diff --git a/src/lib.rs b/src/lib.rs index 15af829..fa869f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -463,6 +463,7 @@ pub fn add_worktree( name: &str, branch_namespace: Option<&str>, track: Option<(&str, &str)>, + no_track: bool, ) -> Result<(), String> { let repo = Repo::open(directory, true).map_err(|error| match error.kind { RepoErrorKind::NotFound => { @@ -471,6 +472,8 @@ pub fn add_worktree( _ => format!("Error opening repo: {}", error), })?; + let config = repo::read_worktree_root_config(directory)?; + if repo.find_worktree(name).is_ok() { return Err(format!("Worktree {} already exists", &name)); } @@ -482,42 +485,101 @@ pub fn add_worktree( let mut remote_branch_exists = false; - let checkout_commit = match track { - Some((remote_name, remote_branch_name)) => { - let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); - match remote_branch { - Ok(branch) => { - remote_branch_exists = true; - branch.to_commit()? - } - Err(_) => { - remote_branch_exists = false; - repo.default_branch()?.to_commit()? + let default_checkout = || { + repo.default_branch()?.to_commit() + }; + + let checkout_commit; + if no_track { + checkout_commit = default_checkout()?; + } else { + match track { + Some((remote_name, remote_branch_name)) => { + let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); + match remote_branch { + Ok(branch) => { + remote_branch_exists = true; + checkout_commit = branch.to_commit()?; + } + Err(_) => { + remote_branch_exists = false; + checkout_commit = default_checkout()?; + } } } - } - None => repo.default_branch()?.to_commit()?, - }; + None => { + match &config { + None => checkout_commit = default_checkout()?, + Some(config) => { + match &config.track { + None => checkout_commit = default_checkout()?, + Some(track_config) => { + if track_config.default { + let remote_branch = repo.find_remote_branch(&track_config.default_remote, name); + match remote_branch { + Ok(branch) => { + remote_branch_exists = true; + checkout_commit = branch.to_commit()?; + } + Err(_) => { + checkout_commit = default_checkout()?; + } + } + } else { + checkout_commit = default_checkout()?; + } + } + } + } + } + } + }; + } let mut target_branch = match repo.find_local_branch(&branch_name) { Ok(branchref) => branchref, Err(_) => repo.create_branch(&branch_name, &checkout_commit)?, }; - if let Some((remote_name, remote_branch_name)) = track { - if remote_branch_exists { - target_branch.set_upstream(remote_name, remote_branch_name)?; - } else { - let mut remote = repo - .find_remote(remote_name) - .map_err(|error| format!("Error getting remote {}: {}", remote_name, error))? - .ok_or_else(|| format!("Remote {} not found", remote_name))?; + if !no_track { + if let Some((remote_name, remote_branch_name)) = track { + if remote_branch_exists { + target_branch.set_upstream(remote_name, remote_branch_name)?; + } else { + let mut remote = repo + .find_remote(remote_name) + .map_err(|error| format!("Error getting remote {}: {}", remote_name, error))? + .ok_or_else(|| format!("Remote {} not found", remote_name))?; - remote.push(&target_branch.name()?, remote_branch_name, &repo)?; + remote.push(&target_branch.name()?, remote_branch_name, &repo)?; - target_branch.set_upstream(remote_name, remote_branch_name)?; + target_branch.set_upstream(remote_name, remote_branch_name)?; + } + } else if let Some(config) = config { + if let Some(track_config) = config.track { + if track_config.default { + let remote_name = track_config.default_remote; + if remote_branch_exists { + target_branch.set_upstream(&remote_name, name)?; + } else { + let remote_branch_name = match track_config.default_remote_prefix { + Some(prefix) => format!("{}{}{}", &prefix, BRANCH_NAMESPACE_SEPARATOR, &name), + None => name.to_string(), + }; + + let mut remote = repo + .find_remote(&remote_name) + .map_err(|error| format!("Error getting remote {}: {}", remote_name, error))? + .ok_or_else(|| format!("Remote {} not found", remote_name))?; + + remote.push(&target_branch.name()?, &remote_branch_name, &repo)?; + + target_branch.set_upstream(&remote_name, &remote_branch_name)?; + } + } + } } - }; + } repo.new_worktree(name, &directory.join(&name), &target_branch)?; diff --git a/src/repo.rs b/src/repo.rs index 23eeb4a..2f7f95b 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -42,10 +42,20 @@ impl RepoError { } } +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TrackingConfig { + pub default: bool, + pub default_remote: String, + pub default_remote_prefix: Option, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct WorktreeRootConfig { pub persistent_branches: Option>, + + pub track: Option, } pub fn read_worktree_root_config(worktree_root: &Path) -> Result, String> {