Skip to content

Commit 07dbfbd

Browse files
authored
Merge pull request #13689 from gitbutlerapp/behind-count
Count 'behind' using octopus merge-base of workspace stack tips
2 parents b8de772 + 3951baf commit 07dbfbd

6 files changed

Lines changed: 142 additions & 18 deletions

File tree

crates/but/tests/but/command/snapshots/status/two-worktrees/status-with-worktrees-verbose.stdout.term.svg

Lines changed: 7 additions & 5 deletions
Loading

crates/but/tests/but/command/snapshots/status/two-worktrees/status-with-worktrees.stdout.term.svg

Lines changed: 7 additions & 5 deletions
Loading

crates/but/tests/but/command/snapshots/status/upstream/status-upstream-summary.stdout.term.svg

Lines changed: 7 additions & 5 deletions
Loading

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,16 +374,44 @@ pub(crate) fn target_to_base_branch(ctx: &Context, target: &Target) -> Result<Ba
374374
// if there are commits ahead of the base branch consider it diverged
375375
let diverged = !diverged_ahead.is_empty();
376376

377-
// gather a list of commits between oid and target.sha
378-
let upstream_commits = first_parent_commit_ids_until(repo, target_commit_id, target.sha)
377+
// Compute the lowest merge base across all workspace stacks and the target.
378+
// Read the workspace ref directly rather than HEAD, since HEAD may not point
379+
// at the workspace commit in all code paths.
380+
let lowest_merge_base = {
381+
let heads: Vec<_> = repo
382+
.find_reference(WORKSPACE_REF_NAME)?
383+
.peel_to_commit()?
384+
.parent_ids()
385+
.map(|id| id.detach())
386+
.collect();
387+
if heads.is_empty() {
388+
// No workspace commit parents — fall back to stored target SHA.
389+
Some(target.sha)
390+
} else {
391+
let base = repo
392+
.merge_base_octopus([heads.as_slice(), &[target_commit_id]].concat())
393+
.context("failed to compute octopus merge-base for workspace stacks")?;
394+
Some(base.object()?.id().detach())
395+
}
396+
};
397+
398+
// Commits on the target branch between its tip and the lowest merge base.
399+
let upstream_commit_ids = lowest_merge_base
400+
.map(|lb| first_parent_commit_ids_until(repo, target_commit_id, lb))
401+
.transpose()
379402
.context("failed to get upstream commits")?
403+
.unwrap_or_default();
404+
405+
let upstream_commits = upstream_commit_ids
380406
.iter()
381407
.map(|id| {
382408
let commit = repo.find_commit(*id)?;
383409
commit_to_remote_commit(&commit)
384410
})
385411
.collect::<Result<Vec<_>>>()?;
386412

413+
let behind = upstream_commits.len();
414+
387415
// get some recent commits
388416
let recent_commits = first_parent_commit_ids_with_limit(repo, target.sha, 20)
389417
.context("failed to get recent commits")?
@@ -429,7 +457,7 @@ pub(crate) fn target_to_base_branch(ctx: &Context, target: &Target) -> Result<Ba
429457
push_remote_url,
430458
base_sha: target.sha,
431459
current_sha: target_commit_id,
432-
behind: upstream_commits.len(),
460+
behind,
433461
upstream_commits,
434462
recent_commits,
435463
last_fetched_ms: ctx

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,42 @@ mod go_back_to_workspace {
243243
assert_eq!(base_two, base);
244244
}
245245
}
246+
247+
mod behind_count {
248+
use super::*;
249+
250+
#[test]
251+
fn behind_reflects_farthest_behind_stack() {
252+
let Test { ctx, .. } = &mut Test::from_fixture("scenario/stacks-with-different-bases.sh");
253+
254+
// HEAD is on branch A (forks from base, 3 behind origin/master).
255+
// set_base_branch picks up A as a workspace stack automatically.
256+
let mut guard = ctx.exclusive_worktree_access();
257+
gitbutler_branch_actions::set_base_branch(
258+
ctx,
259+
&"refs/remotes/origin/master".parse().unwrap(),
260+
guard.write_permission(),
261+
)
262+
.unwrap();
263+
drop(guard);
264+
265+
// Apply C (forks from M2, 1 behind).
266+
let mut guard = ctx.exclusive_worktree_access();
267+
gitbutler_branch_actions::create_virtual_branch_from_branch_with_perm(
268+
ctx,
269+
&"refs/heads/C".parse().unwrap(),
270+
None,
271+
guard.write_permission(),
272+
)
273+
.unwrap();
274+
drop(guard);
275+
276+
// Stack A is farthest behind (3 commits behind origin/master).
277+
// Stack C is 1 commit behind. The behind count should reflect the max.
278+
let base = gitbutler_branch_actions::base::get_base_branch_data(ctx).unwrap();
279+
assert_eq!(
280+
base.behind, 3,
281+
"behind count should match the farthest-behind stack (A, which is 3 commits behind)"
282+
);
283+
}
284+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
set -eu -o pipefail
3+
4+
# Two stacks that fork from different points on master, with origin/master
5+
# advanced past both. This creates a scenario where each stack has
6+
# a different number of "behind" commits relative to the target.
7+
#
8+
# History (after setup):
9+
# origin/master = M3 -> M2 -> M1 -> base
10+
# Stack A forks from base (3 commits behind)
11+
# Stack C forks from M2 (1 commit behind)
12+
#
13+
# HEAD is on branch A so set_base_branch picks it up as a workspace stack.
14+
# Branch C can then be applied separately.
15+
16+
git init -b master local
17+
(cd local
18+
git config commit.gpgsign false
19+
git config user.name gitbutler-test
20+
git config user.email gitbutler-test@example.com
21+
git config gitbutler.storagePath gitbutler
22+
23+
echo "base" >file && git add . && git commit -m "base"
24+
25+
# Stack A forks from base
26+
git checkout -b A
27+
echo "A-work" >a-file && git add . && git commit -m "A: work"
28+
29+
# Advance master to M1, M2, M3
30+
git checkout master
31+
echo "M1" >>file && git add . && git commit -m "M1"
32+
echo "M2" >>file && git add . && git commit -m "M2"
33+
34+
# Stack C forks from M2
35+
git checkout -b C
36+
echo "C-work" >c-file && git add . && git commit -m "C: work"
37+
38+
git checkout master
39+
echo "M3" >>file && git add . && git commit -m "M3"
40+
41+
# Leave HEAD on A so set_base_branch picks it up
42+
git checkout A
43+
)
44+
45+
git init --bare remote
46+
(cd local
47+
remote_path="$(cd ../remote && pwd)"
48+
git remote add origin "$remote_path"
49+
git push origin master
50+
git fetch origin '+refs/heads/*:refs/remotes/origin/*'
51+
)

0 commit comments

Comments
 (0)