Skip to content

Commit e977dc4

Browse files
committed
Abstract stacks for the branch listing to support both workspace engines.
This will help to fix the split-brain we get when branch application fails, yet the toml file claims it's applied. This leads to the user being unable to unapply the branch, while also not seeing it in the workspace.
1 parent a6c6024 commit e977dc4

File tree

2 files changed

+122
-32
lines changed

2 files changed

+122
-32
lines changed

crates/but-workspace/src/ui/ref_info.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ pub struct RemoteTrackingReference {
3939
}
4040

4141
impl RemoteTrackingReference {
42-
fn for_ui(
42+
/// Create a new instance from `ref_name` and `remote_names`, essentially splitting the remote
43+
/// name off the short name.
44+
pub fn for_ui(
4345
ref_name: gix::refs::FullName,
4446
remote_names: &gix::remote::Names,
4547
) -> anyhow::Result<Self> {

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

Lines changed: 119 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ use gitbutler_project::access::WorktreeReadPermission;
1212
use gitbutler_reference::normalize_branch_name;
1313
use gitbutler_reference::RemoteRefname;
1414
use gitbutler_serde::BStringForFrontend;
15-
use gitbutler_stack::{Stack as GitButlerBranch, StackId, Target};
15+
use gitbutler_stack::{Stack, StackId, Target};
1616
use gix::object::tree::diff::Action;
17-
use gix::prelude::{ObjectIdExt, TreeDiffChangeExt};
17+
use gix::prelude::TreeDiffChangeExt;
1818
use gix::reference::Category;
19-
use itertools::Itertools;
2019
use serde::{Deserialize, Serialize};
2120
use std::borrow::Cow;
2221
use std::collections::BTreeSet;
@@ -59,7 +58,6 @@ pub fn list_branches(
5958
repo.object_cache_size_if_unset(1024 * 1024);
6059
let has_filter = filter.is_some();
6160
let filter = filter.unwrap_or_default();
62-
let vb_handle = ctx.project().virtual_branches();
6361
let platform = repo.references()?;
6462
let mut branches: Vec<GroupBranch> = vec![];
6563
for reference in platform.all()?.filter_map(Result::ok) {
@@ -89,8 +87,15 @@ pub fn list_branches(
8987
});
9088
}
9189

90+
let vb_handle = ctx.project().virtual_branches();
9291
let stacks = vb_handle.list_all_stacks()?;
93-
branches.extend(stacks.iter().map(|s| GroupBranch::Virtual(s.clone())));
92+
let remote_names = repo.remote_names();
93+
branches.extend(
94+
stacks
95+
.iter()
96+
.map(|s| GitButlerStack::new(s, &remote_names).map(GroupBranch::Virtual))
97+
.collect::<Result<Vec<_>, _>>()?,
98+
);
9499
let mut branches = combine_branches(branches, &repo, vb_handle.get_default_target()?)?;
95100

96101
// Apply the filter
@@ -264,24 +269,20 @@ fn branch_group_to_branch(
264269
}
265270

266271
// Virtual branch associated with this branch
267-
let virtual_branch_reference = virtual_branch.map(|stack| StackReference {
268-
given_name: stack.name.clone(),
269-
id: stack.id,
270-
in_workspace: stack.in_workspace,
271-
branches: stack
272-
.branches()
273-
.iter()
274-
.filter(|b| !b.archived)
275-
.rev()
276-
.map(|b| b.name())
277-
.cloned()
278-
.collect_vec(),
279-
pull_requests: stack
280-
.branches()
281-
.iter()
282-
.filter(|b| !b.archived)
283-
.filter_map(|b| b.pr_number.map(|pr| (b.name().to_owned(), pr)))
284-
.collect(),
272+
let virtual_branch_reference = virtual_branch.map(|stack| {
273+
let unarchived_branches = stack.unarchived_segments.iter();
274+
StackReference {
275+
given_name: stack.name.clone(),
276+
id: stack.id,
277+
in_workspace: stack.in_workspace,
278+
branches: unarchived_branches
279+
.clone()
280+
.map(|b| b.short_name())
281+
.collect(),
282+
pull_requests: unarchived_branches
283+
.filter_map(|b| b.pr_or_mr.map(|pr| (b.short_name().to_owned(), pr)))
284+
.collect(),
285+
}
285286
});
286287

287288
let mut remotes: Vec<gix::remote::Name<'static>> = Vec::new();
@@ -298,7 +299,7 @@ fn branch_group_to_branch(
298299
// If there is a virtual branch let's get it's head. Alternatively, pick the first local branch and use it's head.
299300
// If there are no local branches, pick the first remote branch.
300301
let head_commit = if let Some(vbranch) = virtual_branch {
301-
Some(vbranch.head_oid(repo)?.attach(repo))
302+
Some(vbranch.head_oid(repo)?)
302303
} else if let Some(mut branch) = local_branches.into_iter().next() {
303304
branch.peel_to_id_in_place_packed(packed).ok()
304305
} else if let Some(mut branch) = remote_branches.into_iter().next() {
@@ -329,11 +330,95 @@ fn branch_group_to_branch(
329330
}
330331

331332
/// A sum type of branch that can be a plain git branch or a virtual branch
332-
#[expect(clippy::large_enum_variant)]
333333
enum GroupBranch<'a> {
334334
Local(gix::Reference<'a>),
335335
Remote(gix::Reference<'a>),
336-
Virtual(GitButlerBranch),
336+
Virtual(GitButlerStack),
337+
}
338+
339+
/// A type to just keep the parts we currently need.
340+
#[derive(Debug)]
341+
struct GitButlerStack {
342+
id: StackId,
343+
/// `true` if the stack is applied to the workspace.
344+
in_workspace: bool,
345+
/// The short name of the top-most segment.
346+
name: String,
347+
/// The full ref name of the top-most segment.
348+
source_refname: Option<gix::refs::FullName>,
349+
/// The full ref name of the remote tracking branch of the top-most segment.
350+
upstream: Option<but_workspace::ui::ref_info::RemoteTrackingReference>,
351+
/// The time at which anything in the stack was last updated.
352+
updated_timestamp_ms: u128,
353+
// All segments of the stack, as long as they are not archived.
354+
// The tip comes first.
355+
unarchived_segments: Vec<GitbutlerStackSegment>,
356+
}
357+
358+
#[derive(Debug)]
359+
struct GitbutlerStackSegment {
360+
/// The name of the segment, without support for these to be anonymous (which is a problem).
361+
tip: gix::refs::FullName,
362+
/// The PR or MR associated with it.
363+
pr_or_mr: Option<usize>,
364+
}
365+
366+
impl GitbutlerStackSegment {
367+
fn short_name(&self) -> String {
368+
self.tip.shorten().to_string()
369+
}
370+
}
371+
372+
impl GitButlerStack {
373+
fn new(s: &Stack, names: &gix::remote::Names) -> anyhow::Result<Self> {
374+
Ok(GitButlerStack {
375+
id: s.id,
376+
in_workspace: s.in_workspace,
377+
name: s.name.clone(),
378+
source_refname: s
379+
.source_refname
380+
.as_ref()
381+
.and_then(|r| r.to_string().try_into().ok()),
382+
upstream: s
383+
.upstream
384+
.as_ref()
385+
.and_then(|r| {
386+
r.to_string().try_into().ok().map(|rn| {
387+
but_workspace::ui::ref_info::RemoteTrackingReference::for_ui(rn, names)
388+
})
389+
})
390+
.transpose()?,
391+
updated_timestamp_ms: s.updated_timestamp_ms,
392+
unarchived_segments: s
393+
.branches()
394+
.iter()
395+
// The tip is at the bottom here.
396+
.rev()
397+
.filter(|s| !s.archived)
398+
.map(|s| GitbutlerStackSegment {
399+
tip: s
400+
.full_name()
401+
.expect("full names are always valid, as their short names were valid"),
402+
pr_or_mr: s.pr_number,
403+
})
404+
.collect(),
405+
})
406+
}
407+
}
408+
409+
impl GitButlerStack {
410+
/// Return the top-most stack's commit id.
411+
fn head_oid<'repo>(&self, repo: &'repo gix::Repository) -> anyhow::Result<gix::Id<'repo>> {
412+
let tip_ref = self
413+
.unarchived_segments
414+
.iter()
415+
.map(|s| s.tip.as_ref())
416+
.next()
417+
.with_context(|| format!("Stack {} didn't have a tip ref name", self.id))?;
418+
repo.find_reference(tip_ref)?
419+
.try_id()
420+
.with_context(|| format!("'{}' was as symbolic reference", tip_ref.shorten()))
421+
}
337422
}
338423

339424
impl fmt::Debug for GroupBranch<'_> {
@@ -352,7 +437,7 @@ impl fmt::Debug for GroupBranch<'_> {
352437
)
353438
.finish(),
354439
GroupBranch::Virtual(branch) => formatter
355-
.debug_struct("GroupBranch::Virtal")
440+
.debug_struct("GroupBranch::Virtual")
356441
.field("0", branch)
357442
.finish(),
358443
}
@@ -370,12 +455,15 @@ impl GroupBranch<'_> {
370455
}
371456
// The identity of a Virtual branch is derived from the source refname, upstream or the branch given name, in that order
372457
GroupBranch::Virtual(branch) => {
373-
let name_from_source = branch.source_refname.as_ref().and_then(|n| n.branch());
374-
let name_from_upstream = branch.upstream.as_ref().map(|n| n.branch());
458+
let name_from_source = branch.source_refname.as_ref().map(|n| n.shorten());
459+
let name_from_upstream = branch
460+
.upstream
461+
.as_ref()
462+
.map(|n| n.display_name.as_str().into());
375463

376464
// If we have a source refname or upstream, use those directly
377465
if let Some(name) = name_from_source.or(name_from_upstream) {
378-
return Some(name.into());
466+
return name.try_into().ok();
379467
}
380468

381469
// Only fall back to the normalized rich name if no source/upstream is available
@@ -515,7 +603,7 @@ pub struct StackReference {
515603
/// List of branches that are part of the stack
516604
/// Ordered from newest to oldest (the most recent branch is first in the list)
517605
pub branches: Vec<String>,
518-
/// Pull Request numbes by branch name associated with the stack
606+
/// Pull Request numbers by branch name associated with the stack
519607
pub pull_requests: HashMap<String, usize>,
520608
}
521609

0 commit comments

Comments
 (0)