Add default tracking configuration

This commit is contained in:
2021-12-23 18:33:14 +01:00
parent 27586b5ff0
commit b183590096
6 changed files with 382 additions and 48 deletions

View File

@@ -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`) new remote tracking branch, using the default branch (either `main` or `master`)
as the base 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 ### Showing the status of your worktrees
There is a handy little command that will show your an overview over all worktrees There is a handy little command that will show your an overview over all worktrees

View File

@@ -4,38 +4,266 @@ from helpers import *
import git import git
import os.path
def test_worktree_add_simple(): def test_worktree_add_simple():
with TempGitRepositoryWorktree() as base_dir: for remote_branch_already_exists in (True, False):
cmd = grm(["wt", "add", "test"], cwd=base_dir) for has_config in (True, False):
assert cmd.returncode == 0 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) if remote_branch_already_exists:
assert len(files) == 2 shell(
assert set(files) == {".git-main-working-tree", "test"} 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")) files = os.listdir(base_dir)
assert not repo.bare if has_config is True:
assert not repo.is_dirty() assert len(files) == 3
assert str(repo.active_branch) == "test" assert set(files) == {
assert repo.active_branch.tracking_branch() is None ".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(): def test_worktree_add_with_tracking():
with TempGitRepositoryWorktree() as base_dir: for remote_branch_already_exists in (True, False):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir) for has_config in (True, False):
print(cmd.stderr) for has_default in (True, False):
assert cmd.returncode == 0 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) if remote_branch_already_exists:
assert len(files) == 2 shell(
assert set(files) == {".git-main-working-tree", "test"} 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")) files = os.listdir(base_dir)
assert not repo.bare if has_config is True:
assert not repo.is_dirty() assert len(files) == 3
assert str(repo.active_branch) == "test" assert set(files) == {
assert str(repo.active_branch.tracking_branch()) == "origin/test" ".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(): def test_worktree_delete():

View File

@@ -102,6 +102,9 @@ pub struct WorktreeAddArgs {
pub branch_namespace: Option<String>, pub branch_namespace: Option<String>,
#[clap(short = 't', long = "track", about = "Remote branch to track")] #[clap(short = 't', long = "track", about = "Remote branch to track")]
pub track: Option<String>, pub track: Option<String>,
#[clap(long = "--no-track", about = "Disable tracking")]
pub no_track: bool,
} }
#[derive(Parser)] #[derive(Parser)]
pub struct WorktreeDeleteArgs { pub struct WorktreeDeleteArgs {

View File

@@ -162,6 +162,7 @@ fn main() {
&action_args.name, &action_args.name,
action_args.branch_namespace.as_deref(), action_args.branch_namespace.as_deref(),
track, track,
action_args.no_track,
) { ) {
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)), Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
Err(error) => { Err(error) => {

View File

@@ -463,6 +463,7 @@ pub fn add_worktree(
name: &str, name: &str,
branch_namespace: Option<&str>, branch_namespace: Option<&str>,
track: Option<(&str, &str)>, track: Option<(&str, &str)>,
no_track: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let repo = Repo::open(directory, true).map_err(|error| match error.kind { let repo = Repo::open(directory, true).map_err(|error| match error.kind {
RepoErrorKind::NotFound => { RepoErrorKind::NotFound => {
@@ -471,6 +472,8 @@ pub fn add_worktree(
_ => format!("Error opening repo: {}", error), _ => format!("Error opening repo: {}", error),
})?; })?;
let config = repo::read_worktree_root_config(directory)?;
if repo.find_worktree(name).is_ok() { if repo.find_worktree(name).is_ok() {
return Err(format!("Worktree {} already exists", &name)); return Err(format!("Worktree {} already exists", &name));
} }
@@ -482,42 +485,101 @@ pub fn add_worktree(
let mut remote_branch_exists = false; let mut remote_branch_exists = false;
let checkout_commit = match track { let default_checkout = || {
Some((remote_name, remote_branch_name)) => { repo.default_branch()?.to_commit()
let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); };
match remote_branch {
Ok(branch) => { let checkout_commit;
remote_branch_exists = true; if no_track {
branch.to_commit()? checkout_commit = default_checkout()?;
} } else {
Err(_) => { match track {
remote_branch_exists = false; Some((remote_name, remote_branch_name)) => {
repo.default_branch()?.to_commit()? 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 => {
None => repo.default_branch()?.to_commit()?, 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) { let mut target_branch = match repo.find_local_branch(&branch_name) {
Ok(branchref) => branchref, Ok(branchref) => branchref,
Err(_) => repo.create_branch(&branch_name, &checkout_commit)?, Err(_) => repo.create_branch(&branch_name, &checkout_commit)?,
}; };
if let Some((remote_name, remote_branch_name)) = track { if !no_track {
if remote_branch_exists { if let Some((remote_name, remote_branch_name)) = track {
target_branch.set_upstream(remote_name, remote_branch_name)?; if remote_branch_exists {
} else { target_branch.set_upstream(remote_name, remote_branch_name)?;
let mut remote = repo } else {
.find_remote(remote_name) let mut remote = repo
.map_err(|error| format!("Error getting remote {}: {}", remote_name, error))? .find_remote(remote_name)
.ok_or_else(|| format!("Remote {} not found", 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)?; repo.new_worktree(name, &directory.join(&name), &target_branch)?;

View File

@@ -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<String>,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct WorktreeRootConfig { pub struct WorktreeRootConfig {
pub persistent_branches: Option<Vec<String>>, pub persistent_branches: Option<Vec<String>>,
pub track: Option<TrackingConfig>,
} }
pub fn read_worktree_root_config(worktree_root: &Path) -> Result<Option<WorktreeRootConfig>, String> { pub fn read_worktree_root_config(worktree_root: &Path) -> Result<Option<WorktreeRootConfig>, String> {