Skip to content

Commit b60d2ef

Browse files
authored
Merge pull request #9878 from Byron/next
V3 apply/unapply
2 parents b8a837b + 42f3e10 commit b60d2ef

File tree

19 files changed

+998
-186
lines changed

19 files changed

+998
-186
lines changed

crates/but-api/src/commands/stack.rs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
use crate::commands::stack::create_reference::Anchor;
21
use crate::error::Error;
32
use anyhow::{Context, anyhow};
43
use but_api_macros::api_cmd;
54
use but_settings::AppSettings;
6-
use but_workspace::branch::{ReferenceAnchor, ReferencePosition};
75
use gitbutler_branch_actions::internal::PushResult;
86
use gitbutler_branch_actions::stack::CreateSeriesRequest;
97
use gitbutler_command_context::CommandContext;
@@ -33,11 +31,11 @@ pub mod create_reference {
3331
pub enum Anchor {
3432
AtCommit {
3533
commit_id: HexHash,
36-
position: but_workspace::branch::ReferencePosition,
34+
position: but_workspace::branch::create_reference::Position,
3735
},
3836
AtReference {
3937
short_name: String,
40-
position: but_workspace::branch::ReferencePosition,
38+
position: but_workspace::branch::create_reference::Position,
4139
},
4240
}
4341
}
@@ -58,17 +56,17 @@ pub fn create_reference(
5856
let anchor = anchor
5957
.map(|anchor| -> Result<_, Error> {
6058
Ok(match anchor {
61-
Anchor::AtCommit {
59+
create_reference::Anchor::AtCommit {
6260
commit_id,
6361
position,
64-
} => but_workspace::branch::ReferenceAnchor::AtCommit {
62+
} => but_workspace::branch::create_reference::Anchor::AtCommit {
6563
commit_id: commit_id.into(),
6664
position,
6765
},
68-
Anchor::AtReference {
66+
create_reference::Anchor::AtReference {
6967
short_name,
7068
position,
71-
} => but_workspace::branch::ReferenceAnchor::AtSegment {
69+
} => but_workspace::branch::create_reference::Anchor::AtSegment {
7270
ref_name: Cow::Owned(
7371
Category::LocalBranch
7472
.to_full_name(short_name.as_str())
@@ -103,7 +101,7 @@ pub fn create_branch(
103101
let project = gitbutler_project::get(project_id)?;
104102
let ctx = CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?;
105103
if ctx.app_settings().feature_flags.ws3 {
106-
use ReferencePosition::Above;
104+
use but_workspace::branch::create_reference::Position::Above;
107105
let mut guard = project.exclusive_worktree_access();
108106
let (repo, mut meta, graph) = ctx.graph_and_meta_mut_and_repo(guard.write_permission())?;
109107
let ws = graph.to_workspace()?;
@@ -120,19 +118,20 @@ pub fn create_branch(
120118

121119
ctx.snapshot_create_dependent_branch(&request.name, guard.write_permission())
122120
.ok();
123-
_ = but_workspace::branch::create_reference(
124-
new_ref.as_ref(),
125-
{
126-
let segment = stack.segments.first().context("BUG: no empty stacks")?;
127-
segment
121+
_ =
122+
but_workspace::branch::create_reference(
123+
new_ref.as_ref(),
124+
{
125+
let segment = stack.segments.first().context("BUG: no empty stacks")?;
126+
segment
128127
.ref_name
129128
.as_ref()
130-
.map(|rn| ReferenceAnchor::AtSegment {
129+
.map(|rn| but_workspace::branch::create_reference::Anchor::AtSegment {
131130
ref_name: Cow::Borrowed(rn.as_ref()),
132131
position: Above,
133132
})
134133
.or_else(|| {
135-
Some(ReferenceAnchor::AtCommit {
134+
Some(but_workspace::branch::create_reference::Anchor::AtCommit {
136135
commit_id: graph.tip_skip_empty(segment.id)?.id,
137136
position: Above,
138137
})
@@ -143,11 +142,11 @@ pub fn create_branch(
143142
couldn't handle stack_id={stack_id:?}, request={request:?}"
144143
)
145144
})?
146-
},
147-
&repo,
148-
&ws,
149-
&mut *meta,
150-
)?;
145+
},
146+
&repo,
147+
&ws,
148+
&mut *meta,
149+
)?;
151150
} else {
152151
// NOTE: locking is built-in here.
153152
gitbutler_branch_actions::stack::create_branch(&ctx, stack_id, request)?;

crates/but-graph/src/init/mod.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ mod remotes;
2121
mod overlay;
2222
mod post;
2323

24+
pub(crate) type Entrypoint = Option<(gix::ObjectId, Option<gix::refs::FullName>)>;
25+
2426
/// A way to define information to be served from memory, instead of from the underlying data source, when
2527
/// [initializing](Graph::from_commit_traversal()) the graph.
2628
#[derive(Debug, Default)]
2729
pub struct Overlay {
30+
entrypoint: Entrypoint,
2831
nonoverriding_references: Vec<gix::refs::Reference>,
2932
meta_branches: Vec<(gix::refs::FullName, ref_metadata::Branch)>,
3033
workspace: Option<(gix::refs::FullName, ref_metadata::Workspace)>,
@@ -141,7 +144,7 @@ impl Graph {
141144
let mut graph = Graph::default();
142145
// It's OK to default-initialise this here as overlays are only used when redoing
143146
// the traversal.
144-
let (_repo, meta) = Overlay::default().into_parts(repo, meta);
147+
let (_repo, meta, _entrypoint) = Overlay::default().into_parts(repo, meta);
145148
graph.insert_root(branch_segment_from_name_and_meta(
146149
Some((ref_name, None)),
147150
&meta,
@@ -228,7 +231,7 @@ impl Graph {
228231
meta: &impl RefMetadata,
229232
options: Options,
230233
) -> anyhow::Result<Self> {
231-
let (repo, meta) = Overlay::default().into_parts(tip.repo, meta);
234+
let (repo, meta, _entrypoint) = Overlay::default().into_parts(tip.repo, meta);
232235
Graph::from_commit_traversal_inner(tip.detach(), &repo, ref_name, &meta, options)
233236
}
234237

@@ -654,16 +657,22 @@ impl Graph {
654657
meta: &impl RefMetadata,
655658
overlay: Overlay,
656659
) -> anyhow::Result<Self> {
657-
let (repo, meta) = overlay.into_parts(repo, meta);
658-
let tip_sidx = self
659-
.entrypoint
660-
.context("BUG: entrypoint must always be set")?
661-
.0;
662-
let tip = self
663-
.tip_skip_empty(tip_sidx)
664-
.context("BUG: entrypoint must eventually point to a commit")?
665-
.id;
666-
let ref_name = self[tip_sidx].ref_name.clone();
660+
let (repo, meta, entrypoint) = overlay.into_parts(repo, meta);
661+
let (tip, ref_name) = match entrypoint {
662+
Some(t) => t,
663+
None => {
664+
let tip_sidx = self
665+
.entrypoint
666+
.context("BUG: entrypoint must always be set")?
667+
.0;
668+
let tip = self
669+
.tip_skip_empty(tip_sidx)
670+
.context("BUG: entrypoint must eventually point to a commit")?
671+
.id;
672+
let ref_name = self[tip_sidx].ref_name.clone();
673+
(tip, ref_name)
674+
}
675+
};
667676
Graph::from_commit_traversal_inner(tip, &repo, ref_name, &meta, self.options.clone())
668677
}
669678

crates/but-graph/src/init/overlay.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::init::Overlay;
21
use crate::init::walk::RefsById;
2+
use crate::init::{Entrypoint, Overlay};
33
use crate::is_workspace_ref_name;
44
use but_core::{RefMetadata, ref_metadata};
55
use gix::prelude::ReferenceExt;
@@ -17,6 +17,17 @@ impl Overlay {
1717
self
1818
}
1919

20+
/// Override the starting position of the traversal by setting it to `id`,
21+
/// and optionally, by providing the `ref_name` that points to `id`.
22+
pub fn with_entrypoint(
23+
mut self,
24+
id: gix::ObjectId,
25+
ref_name: Option<gix::refs::FullName>,
26+
) -> Self {
27+
self.entrypoint = Some((id, ref_name));
28+
self
29+
}
30+
2031
/// Serve the given `branches` metadata from memory, as if they would exist,
2132
/// possibly overriding metadata of a ref that already exists.
2233
pub fn with_branch_metadata_override(
@@ -43,14 +54,15 @@ impl Overlay {
4354
self,
4455
repo: &'repo gix::Repository,
4556
meta: &'meta T,
46-
) -> (OverlayRepo<'repo>, OverlayMetadata<'meta, T>)
57+
) -> (OverlayRepo<'repo>, OverlayMetadata<'meta, T>, Entrypoint)
4758
where
4859
T: RefMetadata,
4960
{
5061
let Overlay {
5162
nonoverriding_references,
5263
meta_branches,
5364
workspace,
65+
entrypoint,
5466
} = self;
5567
(
5668
OverlayRepo {
@@ -62,6 +74,7 @@ impl Overlay {
6274
meta_branches,
6375
workspace,
6476
},
77+
entrypoint,
6578
)
6679
}
6780
}

crates/but-graph/src/projection/workspace.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ pub type CommitOwnerIndexes = (usize, usize, CommitIndex);
6161

6262
/// Utilities
6363
impl Workspace<'_> {
64+
/// Return `true` if the workspace itself is where `HEAD` is pointing to.
65+
/// If `false`, one of the stack-segments is checked out instead.
66+
pub fn is_entrypoint(&self) -> bool {
67+
self.stacks
68+
.iter()
69+
.all(|s| s.segments.iter().all(|s| !s.is_entrypoint))
70+
}
6471
/// Lookup a triple obtained by [`Self::find_owner_indexes_by_commit_id()`] or panic.
6572
pub fn lookup_commit(&self, (stack_idx, seg_idx, cidx): CommitOwnerIndexes) -> &StackCommit {
6673
&self.stacks[stack_idx].segments[seg_idx].commits[cidx]
@@ -149,6 +156,37 @@ impl Workspace<'_> {
149156
})
150157
}
151158

159+
/// Return `true` if `name` is contained in the workspace as segment.
160+
pub fn refname_is_segment(&self, name: &gix::refs::FullNameRef) -> bool {
161+
self.find_segment_and_stack_by_refname(name).is_some()
162+
}
163+
164+
/// Return `true` if the entrypoint.
165+
pub fn is_reachable_from_entrypoint(&self, name: &gix::refs::FullNameRef) -> bool {
166+
if self.is_entrypoint() {
167+
self.refname_is_segment(name)
168+
} else {
169+
let Some((stack, segment_idx)) = self.stacks.iter().find_map(|stack| {
170+
stack
171+
.segments
172+
.iter()
173+
.enumerate()
174+
.find_map(|(idx, segment)| segment.is_entrypoint.then_some((stack, idx)))
175+
}) else {
176+
return false;
177+
};
178+
stack
179+
.segments
180+
.get(segment_idx..)
181+
.into_iter()
182+
.any(|segments| {
183+
segments
184+
.iter()
185+
.any(|s| s.ref_name.as_ref().is_some_and(|rn| rn.as_ref() == name))
186+
})
187+
}
188+
}
189+
152190
/// Try to find `name` in any named [`StackSegment`] and return it along with the stack containing it.
153191
pub fn find_segment_and_stack_by_refname(
154192
&self,
@@ -189,9 +227,9 @@ pub enum WorkspaceKind {
189227
/// The name of the reference pointing to the workspace commit. Useful for deriving the workspace name.
190228
ref_name: gix::refs::FullName,
191229
},
192-
/// Information for when a workspace reference was advanced by hand and does not point to a
193-
/// managed workspace commit anymore.
194-
/// That commit, however, is reachable by following the first parent from the workspace reference.
230+
/// Information for when a workspace reference was *possibly* advanced by hand and does not point to a
231+
/// managed workspace commit (anymore).
232+
/// That workspace commit, may be reachable by following the first parent from the workspace reference.
195233
///
196234
/// Note that the stacks that follow *will* be in unusable if the workspace commit is in a segment below,
197235
/// but typically is usable if there is just a single real stack, or any amount of virtual stacks below

crates/but-graph/tests/fixtures/scenarios.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,14 @@ mkdir ws
298298
create_workspace_commit_once main
299299
)
300300

301+
git init just-init-with-two-branches
302+
(cd just-init-with-two-branches
303+
commit init
304+
git branch A
305+
git branch B
306+
git checkout -b gitbutler/workspace
307+
)
308+
301309
git init just-init-with-branches
302310
(cd just-init-with-branches
303311
commit init && setup_target_to_match_main

crates/but-graph/tests/graph/init/with_workspace.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,58 @@ fn minimal_merge() -> anyhow::Result<()> {
681681
Ok(())
682682
}
683683

684+
#[test]
685+
fn stack_configuration_is_respected_if_one_of_them_is_an_entrypoint() -> anyhow::Result<()> {
686+
let (repo, mut meta) = read_only_in_memory_scenario("ws/just-init-with-two-branches")?;
687+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @"* fafd9d0 (HEAD -> gitbutler/workspace, main, B, A) init");
688+
689+
add_stack_with_segments(&mut meta, 1, "A", StackState::InWorkspace, &[]);
690+
add_stack_with_segments(&mut meta, 2, "B", StackState::InWorkspace, &[]);
691+
692+
let graph = Graph::from_head(
693+
&repo,
694+
&*meta,
695+
standard_options_with_extra_target(&repo, "main"),
696+
)?
697+
.validated()?;
698+
insta::assert_snapshot!(graph_tree(&graph), @r"
699+
└── 👉📕►►►:0[0]:gitbutler/workspace
700+
├── 📙►:2[1]:A
701+
│ └── ►:1[2]:anon:
702+
│ └── ·fafd9d0 (⌂|🏘|1) ►main
703+
└── 📙►:3[1]:B
704+
└── →:1:
705+
");
706+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
707+
📕🏘️⚠️:0:gitbutler/workspace <> ✓! on fafd9d0
708+
├── ≡📙:3:B on fafd9d0
709+
│ └── 📙:3:B
710+
└── ≡📙:2:A on fafd9d0
711+
└── 📙:2:A
712+
");
713+
714+
let (id, ref_name) = id_at(&repo, "B");
715+
let graph = Graph::from_commit_traversal(id, ref_name.clone(), &*meta, standard_options())?
716+
.validated()?;
717+
// TODO: it shouldn't create a dependent branch here, but instead see A as a stack.
718+
// problem is that for stack creation, there is no candidate.
719+
insta::assert_snapshot!(graph_tree(&graph), @r"
720+
└── 📕►►►:1[0]:gitbutler/workspace
721+
└── 👉📙►:0[1]:B
722+
└── 📙►:2[2]:A
723+
└── ·fafd9d0 (⌂|🏘|1) ►main
724+
");
725+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
726+
📕🏘️⚠️:1:gitbutler/workspace <> ✓!
727+
└── ≡👉📙:0:B
728+
├── 👉📙:0:B
729+
└── 📙:2:A
730+
└── ·fafd9d0 (🏘️) ►main
731+
");
732+
733+
Ok(())
734+
}
735+
684736
#[test]
685737
fn just_init_with_branches() -> anyhow::Result<()> {
686738
let (repo, mut meta) = read_only_in_memory_scenario("ws/just-init-with-branches")?;

crates/but-testing/src/command/mod.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use but_core::UnifiedDiff;
33
use but_db::poll::ItemKind;
44
use but_graph::VirtualBranchesTomlMetadata;
55
use but_settings::AppSettings;
6-
use but_workspace::branch::{ReferenceAnchor, ReferencePosition};
6+
use but_workspace::branch::create_reference::{Anchor, Position};
77
use but_workspace::{DiffSpec, HunkHeader};
88
use gitbutler_project::{Project, ProjectId};
99
use gix::bstr::{BString, ByteSlice};
@@ -601,22 +601,21 @@ pub fn create_reference(
601601
) -> anyhow::Result<()> {
602602
let (repo, project, graph, mut meta) =
603603
repo_and_maybe_project_and_graph(args, RepositoryOpenMode::General)?;
604-
let resolve =
605-
|spec: &str, position: ReferencePosition| -> anyhow::Result<ReferenceAnchor<'_>> {
606-
Ok(match repo.try_find_reference(spec)? {
607-
None => ReferenceAnchor::AtCommit {
608-
commit_id: repo.rev_parse_single(spec)?.detach(),
609-
position,
610-
},
611-
Some(rn) => ReferenceAnchor::AtSegment {
612-
ref_name: Cow::Owned(rn.inner.name),
613-
position,
614-
},
615-
})
616-
};
604+
let resolve = |spec: &str, position: Position| -> anyhow::Result<Anchor<'_>> {
605+
Ok(match repo.try_find_reference(spec)? {
606+
None => Anchor::AtCommit {
607+
commit_id: repo.rev_parse_single(spec)?.detach(),
608+
position,
609+
},
610+
Some(rn) => Anchor::AtSegment {
611+
ref_name: Cow::Owned(rn.inner.name),
612+
position,
613+
},
614+
})
615+
};
617616
let anchor = above
618-
.map(|spec| resolve(spec, ReferencePosition::Above))
619-
.or_else(|| below.map(|spec| resolve(spec, ReferencePosition::Below)))
617+
.map(|spec| resolve(spec, Position::Above))
618+
.or_else(|| below.map(|spec| resolve(spec, Position::Below)))
620619
.transpose()?;
621620

622621
let new_ref = Category::LocalBranch.to_full_name(short_name)?;

0 commit comments

Comments
 (0)