Add default tracking configuration
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
112
src/lib.rs
112
src/lib.rs
@@ -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)?;
|
||||||
|
|
||||||
|
|||||||
10
src/repo.rs
10
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<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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user