Skip to content

Commit 01270f2

Browse files
committed
Rebuild workspace commit when upstream target is already up-to-date
When `default_target.sha` in TOML already matches origin/master (e.g. after `bootstrap_default_target_if_missing` writes the remote tip directly), `integrate_upstream` returned `UpToDate` and skipped rebuilding the workspace commit. This left the workspace commit parented on an older base while the UI showed commits behind. Now we always rebuild the workspace commit in the `UpToDate` path so the worktree reflects the current target even when the metadata was already advanced.
1 parent e0beb31 commit 01270f2

3 files changed

Lines changed: 120 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
3+
source "${BASH_SOURCE[0]%/*}/shared.sh"
4+
5+
git init --initial-branch=master
6+
echo "Branch whose head is a merge commit that merged master into feature - already integrated upstream" >.git/description
7+
8+
commit o1
9+
git branch o1
10+
11+
# Feature branch with one commit
12+
git checkout -b feature
13+
commit feature-work
14+
git branch feature-work
15+
16+
# Master advances
17+
git checkout master
18+
commit o2
19+
20+
# Feature merges master into itself (common pattern: "catch up with master")
21+
git checkout feature
22+
git merge --no-ff -m "Merge master into feature" master
23+
24+
# Master advances further and also merges the feature branch
25+
git checkout master
26+
git merge --no-ff -m "Merge feature" feature
27+
git branch merged-point
28+
commit o3
29+
30+
setup_remote_tracking master master
31+
cat <<EOF >>.git/config
32+
[remote "origin"]
33+
url = ./fake/local/path/which-is-fine-as-we-dont-fetch-or-push
34+
fetch = +refs/heads/*:refs/remotes/origin/*
35+
36+
[branch "master"]
37+
remote = "origin"
38+
merge = refs/heads/master
39+
EOF
40+
41+
git checkout feature
42+
create_workspace_commit_once feature

crates/gitbutler-branch-actions/src/upstream_integration.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,17 @@ pub(crate) fn integrate_upstream(
568568
let statuses = upstream_integration_statuses(&context)?;
569569

570570
let StackStatuses::UpdatesRequired { statuses, .. } = statuses else {
571+
// Even when stacks are up-to-date with the target, the workspace
572+
// commit itself may be parented on an older base (e.g. after
573+
// `bootstrap_default_target_if_missing` advanced `default_target.sha`
574+
// without rebuilding the workspace commit). Rebuild it so the
575+
// worktree reflects the current target.
576+
let virtual_branches_state = VirtualBranchesHandle::new(ctx.project_data_dir());
577+
crate::integration::update_workspace_commit_with_vb_state(
578+
&virtual_branches_state,
579+
ctx,
580+
false,
581+
)?;
571582
return Ok(IntegrationOutcome {
572583
deleted_branches: vec![],
573584
});

crates/gitbutler-branch-actions/tests/branch-actions/virtual_branches/apply_virtual_branch.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,3 +995,70 @@ fn integrate_upstream_with_inter_stack_tree_conflict() {
995995
);
996996
}
997997
}
998+
999+
/// Regression test: when `default_target.sha` in TOML already matches the
1000+
/// remote tip (e.g. written by `bootstrap_default_target_if_missing`) but the
1001+
/// workspace commit is still parented on an older base, integrate_upstream
1002+
/// must still rebuild the workspace commit so it sits on top of the target.
1003+
#[test]
1004+
fn integrate_upstream_rebuilds_stale_workspace_commit() {
1005+
let Test { repo, ctx, .. } = &mut Test::default();
1006+
1007+
// Create initial + second commit, push both
1008+
fs::write(repo.path().join("file.txt"), "initial").unwrap();
1009+
repo.commit_all("initial");
1010+
repo.push();
1011+
fs::write(repo.path().join("file.txt"), "second").unwrap();
1012+
let upstream_head = repo.commit_all("second commit");
1013+
repo.push();
1014+
1015+
// Reset local back so set_base_branch sees a delta
1016+
repo.reset_hard(Some(
1017+
but_testsupport::open_repo(repo.path())
1018+
.unwrap()
1019+
.rev_parse_single("HEAD~1")
1020+
.unwrap()
1021+
.detach(),
1022+
));
1023+
1024+
// Set up GitButler — workspace commit will be parented on `initial`
1025+
let mut guard = ctx.exclusive_worktree_access();
1026+
gitbutler_branch_actions::set_base_branch(
1027+
ctx,
1028+
&"refs/remotes/origin/master".parse().unwrap(),
1029+
guard.write_permission(),
1030+
)
1031+
.unwrap();
1032+
drop(guard);
1033+
1034+
// Now simulate the bug condition: advance TOML target.sha to match
1035+
// origin/master without rebuilding the workspace commit. This is what
1036+
// bootstrap_default_target_if_missing does.
1037+
{
1038+
let mut meta = ctx.legacy_meta().unwrap();
1039+
let mut target = meta.data().default_target.clone().unwrap();
1040+
target.sha = upstream_head;
1041+
meta.set_default_target(target).unwrap();
1042+
ctx.invalidate_workspace_cache().unwrap();
1043+
}
1044+
1045+
// Verify the desync: TOML matches remote, but workspace is on old base
1046+
let base = gitbutler_branch_actions::base::get_base_branch_data(ctx).unwrap();
1047+
assert_eq!(base.base_sha, upstream_head, "TOML should be at remote tip");
1048+
assert!(
1049+
base.behind > 0,
1050+
"workspace commit is on old base, should show behind"
1051+
);
1052+
1053+
// Before the fix, this would return UpToDate and leave the workspace stale.
1054+
// Now it rebuilds the workspace commit even when the target is up-to-date.
1055+
let review_map = HashMap::new();
1056+
gitbutler_branch_actions::integrate_upstream(ctx, &[], None, &review_map)
1057+
.expect("integrate_upstream should succeed");
1058+
1059+
let base_after = gitbutler_branch_actions::base::get_base_branch_data(ctx).unwrap();
1060+
assert_eq!(
1061+
base_after.behind, 0,
1062+
"workspace should be rebuilt on current target"
1063+
);
1064+
}

0 commit comments

Comments
 (0)