Skip to content

Commit 5da1d95

Browse files
authored
Merge pull request #10402 from Byron/fix
Use the new workspace engine when listing branches if `ws3` is enabled
2 parents 0e3dbba + c72c17a commit 5da1d95

File tree

4 files changed

+229
-40
lines changed

4 files changed

+229
-40
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: 192 additions & 36 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,7 +87,36 @@ pub fn list_branches(
8987
});
9088
}
9189

92-
let stacks = vb_handle.list_all_stacks()?;
90+
let vb_handle = ctx.project().virtual_branches();
91+
let remote_names = repo.remote_names();
92+
let stacks = if ctx.app_settings().feature_flags.ws3 {
93+
if let Some(workspace_ref) = repo.try_find_reference("refs/heads/gitbutler/workspace")? {
94+
// Let's get this here for convenience, and hope this isn't ever called by a writer (or there will be a deadlock).
95+
let read_guard = ctx.project().shared_worktree_access();
96+
let meta = ctx.meta(read_guard.read_permission())?;
97+
let info = but_workspace::ref_info(
98+
workspace_ref,
99+
&*meta,
100+
but_workspace::ref_info::Options {
101+
traversal: but_graph::init::Options::limited(),
102+
expensive_commit_info: false,
103+
},
104+
)?;
105+
info.stacks
106+
.into_iter()
107+
.map(|s| GitButlerStack::new(s, &remote_names))
108+
.collect::<Result<Vec<_>, _>>()?
109+
} else {
110+
Vec::new()
111+
}
112+
} else {
113+
vb_handle
114+
.list_all_stacks()?
115+
.iter()
116+
.map(|s| GitButlerStack::new_from_old(s, &remote_names))
117+
.collect::<Result<Vec<_>, _>>()?
118+
};
119+
93120
branches.extend(stacks.iter().map(|s| GroupBranch::Virtual(s.clone())));
94121
let mut branches = combine_branches(branches, &repo, vb_handle.get_default_target()?)?;
95122

@@ -130,13 +157,13 @@ pub fn list_branches(
130157
// To do this, we build up a list of all the branch identities that are
131158
// part of a stack and then filter out any branches that have been grouped
132159
// without a stack and match one of these identities.
133-
let branch_identities_to_exclude = stacks
134-
.iter()
160+
let branch_identities_to_exclude: HashSet<BString> = stacks
161+
.into_iter()
135162
.flat_map(|s| {
136-
s.branches()
163+
s.unarchived_segments
137164
.into_iter()
138-
.map(|b| BString::from(b.name().to_owned()))
139-
.chain([BString::from(s.name.to_owned())])
165+
.map(|b| b.short_name().into())
166+
.chain(Some(s.name.into()))
140167
})
141168
.collect::<HashSet<_>>();
142169

@@ -264,24 +291,20 @@ fn branch_group_to_branch(
264291
}
265292

266293
// 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(),
294+
let virtual_branch_reference = virtual_branch.map(|stack| {
295+
let unarchived_branches = stack.unarchived_segments.iter();
296+
StackReference {
297+
given_name: stack.name.clone(),
298+
id: stack.id,
299+
in_workspace: stack.in_workspace,
300+
branches: unarchived_branches
301+
.clone()
302+
.map(|b| b.short_name())
303+
.collect(),
304+
pull_requests: unarchived_branches
305+
.filter_map(|b| b.pr_or_mr.map(|pr| (b.short_name().to_owned(), pr)))
306+
.collect(),
307+
}
285308
});
286309

287310
let mut remotes: Vec<gix::remote::Name<'static>> = Vec::new();
@@ -298,7 +321,7 @@ fn branch_group_to_branch(
298321
// If there is a virtual branch let's get it's head. Alternatively, pick the first local branch and use it's head.
299322
// If there are no local branches, pick the first remote branch.
300323
let head_commit = if let Some(vbranch) = virtual_branch {
301-
Some(vbranch.head_oid(repo)?.attach(repo))
324+
Some(vbranch.head_oid(repo)?)
302325
} else if let Some(mut branch) = local_branches.into_iter().next() {
303326
branch.peel_to_id_in_place_packed(packed).ok()
304327
} else if let Some(mut branch) = remote_branches.into_iter().next() {
@@ -329,11 +352,141 @@ fn branch_group_to_branch(
329352
}
330353

331354
/// A sum type of branch that can be a plain git branch or a virtual branch
332-
#[expect(clippy::large_enum_variant)]
333355
enum GroupBranch<'a> {
334356
Local(gix::Reference<'a>),
335357
Remote(gix::Reference<'a>),
336-
Virtual(GitButlerBranch),
358+
Virtual(GitButlerStack),
359+
}
360+
361+
/// A type to just keep the parts we currently need.
362+
#[derive(Debug, Clone)]
363+
struct GitButlerStack {
364+
id: StackId,
365+
/// `true` if the stack is applied to the workspace.
366+
in_workspace: bool,
367+
/// The short name of the top-most segment.
368+
name: String,
369+
/// The full ref name of the top-most segment.
370+
source_refname: Option<gix::refs::FullName>,
371+
/// The full ref name of the remote tracking branch of the top-most segment.
372+
upstream: Option<but_workspace::ui::ref_info::RemoteTrackingReference>,
373+
/// The time at which anything in the stack was last updated.
374+
updated_timestamp_ms: u128,
375+
// All segments of the stack, as long as they are not archived.
376+
// The tip comes first.
377+
unarchived_segments: Vec<GitbutlerStackSegment>,
378+
}
379+
380+
#[derive(Debug, Clone)]
381+
struct GitbutlerStackSegment {
382+
/// The name of the segment, without support for these to be anonymous (which is a problem).
383+
tip: gix::refs::FullName,
384+
/// The PR or MR associated with it.
385+
pr_or_mr: Option<usize>,
386+
}
387+
388+
impl GitbutlerStackSegment {
389+
fn short_name(&self) -> String {
390+
self.tip.shorten().to_string()
391+
}
392+
}
393+
394+
impl GitButlerStack {
395+
fn new(
396+
stack: but_workspace::branch::Stack,
397+
names: &gix::remote::Names,
398+
) -> anyhow::Result<Self> {
399+
let first_segment = stack.segments.first();
400+
Ok(GitButlerStack {
401+
id: stack.id.context("Can't handle stacks without ID yet")?,
402+
// The ones we have reachable are never
403+
in_workspace: true,
404+
name: stack
405+
.name()
406+
.map(|rn| rn.shorten().to_string())
407+
// Hack it - the datastructure isn't suitable and this needs a `gitbutler->but` port.
408+
.unwrap_or_default(),
409+
source_refname: stack.ref_name().map(|rn| rn.to_owned()),
410+
upstream: first_segment
411+
.and_then(|s| {
412+
s.remote_tracking_ref_name.as_ref().map(|rn| {
413+
but_workspace::ui::ref_info::RemoteTrackingReference::for_ui(
414+
rn.clone(),
415+
names,
416+
)
417+
})
418+
})
419+
.transpose()?,
420+
updated_timestamp_ms: first_segment
421+
.and_then(|s| {
422+
let md = s.metadata.as_ref()?;
423+
Some(md.ref_info.updated_at?.seconds as u128 * 1_000)
424+
})
425+
.unwrap_or_default(),
426+
unarchived_segments: stack
427+
.segments
428+
.iter()
429+
.map(|s| GitbutlerStackSegment {
430+
tip: s.ref_name.clone().unwrap_or_else(|| {
431+
gix::refs::FullName::try_from(
432+
"refs/heads/unnamed-ref-and-we-fake-a-name-fix-me",
433+
)
434+
.expect("known to be valid statically")
435+
}),
436+
pr_or_mr: s.metadata.as_ref().and_then(|md| md.review.pull_request),
437+
})
438+
.collect(),
439+
})
440+
}
441+
fn new_from_old(stack: &Stack, names: &gix::remote::Names) -> anyhow::Result<Self> {
442+
Ok(GitButlerStack {
443+
id: stack.id,
444+
in_workspace: stack.in_workspace,
445+
name: stack.name.clone(),
446+
source_refname: stack
447+
.source_refname
448+
.as_ref()
449+
.and_then(|r| r.to_string().try_into().ok()),
450+
upstream: stack
451+
.upstream
452+
.as_ref()
453+
.and_then(|r| {
454+
r.to_string().try_into().ok().map(|rn| {
455+
but_workspace::ui::ref_info::RemoteTrackingReference::for_ui(rn, names)
456+
})
457+
})
458+
.transpose()?,
459+
updated_timestamp_ms: stack.updated_timestamp_ms,
460+
unarchived_segments: stack
461+
.branches()
462+
.iter()
463+
// The tip is at the bottom here.
464+
.rev()
465+
.filter(|s| !s.archived)
466+
.map(|s| GitbutlerStackSegment {
467+
tip: s
468+
.full_name()
469+
.expect("full names are always valid, as their short names were valid"),
470+
pr_or_mr: s.pr_number,
471+
})
472+
.collect(),
473+
})
474+
}
475+
}
476+
477+
impl GitButlerStack {
478+
/// Return the top-most stack's commit id.
479+
fn head_oid<'repo>(&self, repo: &'repo gix::Repository) -> anyhow::Result<gix::Id<'repo>> {
480+
let tip_ref = self
481+
.unarchived_segments
482+
.iter()
483+
.map(|s| s.tip.as_ref())
484+
.next()
485+
.with_context(|| format!("Stack {} didn't have a tip ref name", self.id))?;
486+
repo.find_reference(tip_ref)?
487+
.try_id()
488+
.with_context(|| format!("'{}' was as symbolic reference", tip_ref.shorten()))
489+
}
337490
}
338491

339492
impl fmt::Debug for GroupBranch<'_> {
@@ -352,7 +505,7 @@ impl fmt::Debug for GroupBranch<'_> {
352505
)
353506
.finish(),
354507
GroupBranch::Virtual(branch) => formatter
355-
.debug_struct("GroupBranch::Virtal")
508+
.debug_struct("GroupBranch::Virtual")
356509
.field("0", branch)
357510
.finish(),
358511
}
@@ -370,12 +523,15 @@ impl GroupBranch<'_> {
370523
}
371524
// The identity of a Virtual branch is derived from the source refname, upstream or the branch given name, in that order
372525
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());
526+
let name_from_source = branch.source_refname.as_ref().map(|n| n.shorten());
527+
let name_from_upstream = branch
528+
.upstream
529+
.as_ref()
530+
.map(|n| n.display_name.as_str().into());
375531

376532
// If we have a source refname or upstream, use those directly
377533
if let Some(name) = name_from_source.or(name_from_upstream) {
378-
return Some(name.into());
534+
return name.try_into().ok();
379535
}
380536

381537
// Only fall back to the normalized rich name if no source/upstream is available
@@ -515,7 +671,7 @@ pub struct StackReference {
515671
/// List of branches that are part of the stack
516672
/// Ordered from newest to oldest (the most recent branch is first in the list)
517673
pub branches: Vec<String>,
518-
/// Pull Request numbes by branch name associated with the stack
674+
/// Pull Request numbers by branch name associated with the stack
519675
pub pull_requests: HashMap<String, usize>,
520676
}
521677

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ fn one_vbranch_in_workspace_one_commit() -> Result<()> {
4545
#[test]
4646
fn two_vbranches_in_workspace_one_commit() -> Result<()> {
4747
init_env();
48-
let ctx = project_ctx("two-vbranches-in-workspace-one-applied")?;
48+
let ctx = project_ctx_without_ws3("two-vbranches-in-workspace-one-applied")?;
4949
let list = list_branches(
5050
&ctx,
5151
Some(BranchListingFilter {
@@ -92,7 +92,7 @@ fn two_vbranches_in_workspace_one_commit() -> Result<()> {
9292
#[test]
9393
fn one_feature_branch_and_one_vbranch_in_workspace_one_commit() -> Result<()> {
9494
init_env();
95-
let ctx = project_ctx("a-vbranch-named-like-target-branch-short-name")?;
95+
let ctx = project_ctx_without_ws3("a-vbranch-named-like-target-branch-short-name")?;
9696
let list = list_branches(&ctx, None)?;
9797
assert_eq!(
9898
list.len(),
@@ -118,7 +118,7 @@ fn one_feature_branch_and_one_vbranch_in_workspace_one_commit() -> Result<()> {
118118
#[test]
119119
fn one_branch_in_workspace_multiple_remotes() -> Result<()> {
120120
init_env();
121-
let ctx = project_ctx("one-vbranch-in-workspace-two-remotes")?;
121+
let ctx = project_ctx_without_ws3("one-vbranch-in-workspace-two-remotes")?;
122122
let list = list_branches(&ctx, None)?;
123123
assert_eq!(list.len(), 1, "a single virtual branch");
124124

@@ -138,6 +138,8 @@ fn one_branch_in_workspace_multiple_remotes() -> Result<()> {
138138

139139
mod util {
140140
use anyhow::Result;
141+
use but_settings::app_settings::FeatureFlags;
142+
use but_settings::AppSettings;
141143
use gitbutler_branch::BranchIdentity;
142144
use gitbutler_branch_actions::{BranchListing, BranchListingFilter};
143145
use gitbutler_command_context::CommandContext;
@@ -221,6 +223,17 @@ mod util {
221223
gitbutler_testsupport::read_only::fixture("for-listing.sh", name)
222224
}
223225

226+
pub fn project_ctx_without_ws3(name: &str) -> Result<CommandContext> {
227+
gitbutler_testsupport::read_only::fixture_with_features(
228+
"for-listing.sh",
229+
name,
230+
FeatureFlags {
231+
ws3: false,
232+
..AppSettings::default().feature_flags
233+
},
234+
)
235+
}
236+
224237
pub fn list_branches(
225238
ctx: &CommandContext,
226239
filter: Option<BranchListingFilter>,
@@ -230,4 +243,5 @@ mod util {
230243
Ok(branches)
231244
}
232245
}
246+
use crate::virtual_branches::list::util::project_ctx_without_ws3;
233247
pub use util::{assert_equal, init_env, list_branches, project_ctx, ExpectedBranchListing};

0 commit comments

Comments
 (0)