Skip to content

Commit 5bdb832

Browse files
fix(remove): clear a branch's marker when wt deletes the branch (#4684)
1 parent 786978f commit 5bdb832

9 files changed

Lines changed: 118 additions & 0 deletions

File tree

‎docs/src/content/docs/config.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,8 @@ Stored in git config as `worktrunk.state.<branch>.marker`. Set directly with:
14641464
$ git config worktrunk.state.feature.marker '{"marker":"🚧","set_at":0}'
14651465
```
14661466

1467+
When `wt` deletes a branch, it clears that branch's marker.
1468+
14671469
Without a subcommand, runs `get` for the current branch. For `--branch`, use `get --branch=NAME`.
14681470

14691471
### Command reference

‎docs/src/content/docs/faq.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ Use `-D` to force-delete branches with unmerged changes. Use `--no-delete-branch
210210

211211
A branch checked out in a second worktree is retained regardless, `-D` included. Deleting it would leave that worktree unable to resolve `HEAD`; only `git worktree add --force` produces that state.
212212

213+
Deleting a branch also clears its [marker](/config/#wt-config-state-marker), so a later branch with the same name starts without one.
214+
213215
### Other cleanup
214216

215217
- `wt merge` / `wt step push` — the target branch's checked-out worktree is updated to the merged commits, so a file those commits delete disappears from it, and an ignored file at a path they track is overwritten — the same result a `git merge` run in that worktree would produce. Uncommitted changes at paths the merge doesn't touch stay in place, staged or not; one at a path it does touch refuses the merge upfront, naming the file

‎plugins/worktrunk/skills/worktrunk/reference/config.md‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎plugins/worktrunk/skills/worktrunk/reference/faq.md‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎skills/worktrunk/reference/config.md‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎skills/worktrunk/reference/faq.md‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/cli/config.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,8 @@ Stored in git config as `worktrunk.state.<branch>.marker`. Set directly with:
12421242
$ git config worktrunk.state.feature.marker '{"marker":"🚧","set_at":0}'
12431243
```
12441244
1245+
When `wt` deletes a branch, it clears that branch's marker.
1246+
12451247
Without a subcommand, runs `get` for the current branch. For `--branch`, use `get --branch=NAME`."#
12461248
)]
12471249
Marker {

‎src/git/remove.rs‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ pub fn delete_branch_if_safe(
645645
// behavior for the Worktree path). The user explicitly chose -D.
646646
if force_delete {
647647
repo.run_command(&["branch", "-D", "--", branch_name])?;
648+
clear_deleted_branch_marker(repo, branch_name);
648649
return Ok(BranchDeletionResult {
649650
outcome: BranchDeletionOutcome::ForceDeleted,
650651
integration_target: target.to_string(),
@@ -673,13 +674,32 @@ pub fn delete_branch_if_safe(
673674
}
674675
None => BranchDeletionOutcome::NotDeleted,
675676
};
677+
if matches!(outcome, BranchDeletionOutcome::Integrated(_)) {
678+
clear_deleted_branch_marker(repo, branch_name);
679+
}
676680

677681
Ok(BranchDeletionResult {
678682
outcome,
679683
integration_target: effective_target,
680684
})
681685
}
682686

687+
/// Drop the activity marker of a branch `wt` just deleted.
688+
///
689+
/// Markers are keyed by branch name in shared git config, so one left behind
690+
/// would reappear on a later branch reusing the name. Best effort: the branch
691+
/// is already gone, so a failed unset is logged rather than surfaced.
692+
fn clear_deleted_branch_marker(repo: &Repository, branch_name: &str) {
693+
// `<name>.vars.marker` is also branch `<name>`'s var `marker`; only
694+
// marker JSON belongs to the deleted branch (as in `clear_all_markers`).
695+
if repo.branch_marker(branch_name).is_none() {
696+
return;
697+
}
698+
if let Err(e) = repo.unset_config(&format!("worktrunk.state.{branch_name}.marker")) {
699+
tracing::debug!(error = %e, "Failed to clear marker for deleted branch {branch_name}: {e}");
700+
}
701+
}
702+
683703
/// Find a live checkout of `branch` using a new repository cache.
684704
///
685705
/// Removal planning intentionally caches `git worktree list`, but branch
@@ -1158,6 +1178,88 @@ mod tests {
11581178
assert!(!exit.success(), "branch should have been deleted");
11591179
}
11601180

1181+
/// Deleting a branch drops its marker, so a later branch reusing the name
1182+
/// doesn't inherit it; a retained branch keeps its marker.
1183+
#[test]
1184+
fn deletion_clears_branch_marker() {
1185+
let test = TestRepo::with_initial_commit();
1186+
let repo = Repository::at(test.root_path()).unwrap();
1187+
for branch in ["integrated", "forced", "unmerged"] {
1188+
test.run_git(&["branch", branch]);
1189+
repo.set_config(
1190+
&format!("worktrunk.state.{branch}.marker"),
1191+
r#"{"marker":"🤖","set_at":0}"#,
1192+
)
1193+
.unwrap();
1194+
}
1195+
test.run_git(&["checkout", "unmerged"]);
1196+
std::fs::write(test.root_path().join("work.txt"), "work\n").unwrap();
1197+
test.run_git(&["add", "work.txt"]);
1198+
test.run_git(&["commit", "-m", "unmerged work"]);
1199+
test.run_git(&["checkout", "main"]);
1200+
1201+
let snapshot = repo.capture_refs().unwrap();
1202+
for (branch, force) in [("integrated", false), ("forced", true), ("unmerged", false)] {
1203+
delete_branch_if_safe(&repo, &snapshot, branch, "main", force).unwrap();
1204+
}
1205+
1206+
let fresh = Repository::at(test.root_path()).unwrap();
1207+
assert_eq!(fresh.branch_marker("integrated"), None);
1208+
assert_eq!(fresh.branch_marker("forced"), None);
1209+
assert_eq!(fresh.branch_marker("unmerged").as_deref(), Some("🤖"));
1210+
}
1211+
1212+
/// Deleting `<name>.vars` leaves branch `<name>`'s var named `marker`,
1213+
/// which shares the key `worktrunk.state.<name>.vars.marker`.
1214+
#[test]
1215+
fn deletion_keeps_var_sharing_marker_key() {
1216+
let test = TestRepo::with_initial_commit();
1217+
let repo = Repository::at(test.root_path()).unwrap();
1218+
test.run_git(&["branch", "feature.vars"]);
1219+
repo.set_config("worktrunk.state.feature.vars.marker", "plain value")
1220+
.unwrap();
1221+
1222+
let snapshot = repo.capture_refs().unwrap();
1223+
delete_branch_if_safe(&repo, &snapshot, "feature.vars", "main", true).unwrap();
1224+
1225+
let fresh = Repository::at(test.root_path()).unwrap();
1226+
assert_eq!(
1227+
fresh
1228+
.config_last("worktrunk.state.feature.vars.marker")
1229+
.unwrap()
1230+
.as_deref(),
1231+
Some("plain value")
1232+
);
1233+
}
1234+
1235+
/// A marker that can't be unset doesn't fail a deletion that already
1236+
/// happened; Git itself only warns when the locked config blocks its own
1237+
/// branch-section cleanup.
1238+
#[test]
1239+
fn deletion_survives_marker_unset_failure() {
1240+
let test = TestRepo::with_initial_commit();
1241+
let repo = Repository::at(test.root_path()).unwrap();
1242+
test.run_git(&["branch", "doomed"]);
1243+
repo.set_config(
1244+
"worktrunk.state.doomed.marker",
1245+
r#"{"marker":"🤖","set_at":0}"#,
1246+
)
1247+
.unwrap();
1248+
let lock = test.root_path().join(".git/config.lock");
1249+
std::fs::write(&lock, "").unwrap();
1250+
1251+
let snapshot = repo.capture_refs().unwrap();
1252+
let result = delete_branch_if_safe(&repo, &snapshot, "doomed", "main", true).unwrap();
1253+
std::fs::remove_file(&lock).unwrap();
1254+
1255+
assert!(matches!(
1256+
result.outcome,
1257+
BranchDeletionOutcome::ForceDeleted
1258+
));
1259+
let fresh = Repository::at(test.root_path()).unwrap();
1260+
assert_eq!(fresh.branch_marker("doomed").as_deref(), Some("🤖"));
1261+
}
1262+
11611263
/// A packed-ref lock failure must preserve the branch and the original
11621264
/// Git error; an unchanged ref is not evidence of branch movement.
11631265
#[test]

‎tests/snapshots/integration__integration_tests__help__help_config_state_marker.snap‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Stored in git config as worktrunk.state.<branch>.marker. Set directly wi
9797

9898
  git config worktrunk.state.feature.marker '{"marker":"🚧","set_at":0}'
9999

100+
When wt deletes a branch, it clears that branch's marker.
101+
100102
Without a subcommand, runs get for the current branch. For --branch, use get --branch=NAME.
101103

102104
----- stderr -----

0 commit comments

Comments
 (0)