Skip to content

Commit c76ae73

Browse files
committed
Basic apply and unapply
The idea is to develop both at the same time so it's easier to test that 'unapply' truly goes back to the (mostly) original state.
1 parent 456b28e commit c76ae73

16 files changed

+621
-64
lines changed

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

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

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: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ impl Workspace<'_> {
149149
})
150150
}
151151

152+
/// Return `true` if `name` is contained in the workspace as segment.
153+
pub fn refname_is_segment(&self, name: &gix::refs::FullNameRef) -> bool {
154+
self.find_segment_and_stack_by_refname(name).is_some()
155+
}
156+
152157
/// Try to find `name` in any named [`StackSegment`] and return it along with the stack containing it.
153158
pub fn find_segment_and_stack_by_refname(
154159
&self,
@@ -189,9 +194,9 @@ pub enum WorkspaceKind {
189194
/// The name of the reference pointing to the workspace commit. Useful for deriving the workspace name.
190195
ref_name: gix::refs::FullName,
191196
},
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.
197+
/// Information for when a workspace reference was *possibly* advanced by hand and does not point to a
198+
/// managed workspace commit (anymore).
199+
/// That workspace commit, may be reachable by following the first parent from the workspace reference.
195200
///
196201
/// Note that the stacks that follow *will* be in unusable if the workspace commit is in a segment below,
197202
/// but typically is usable if there is just a single real stack, or any amount of virtual stacks below
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use std::borrow::Cow;
2+
3+
/// Returned by [function::apply()].
4+
pub struct Outcome<'graph> {
5+
/// The newly created graph, if owned, useful to project a workspace and see how the workspace looks like with the branch applied.
6+
/// If borrowed, the graph already contains the desired branch and nothing had to be applied.
7+
pub graph: Cow<'graph, but_graph::Graph>,
8+
/// `true` if we created the given workspace ref as it didn't exist yet.
9+
pub workspace_ref_created: bool,
10+
}
11+
12+
impl Outcome<'_> {
13+
/// Return `true` if a new graph traversal was performed, which always is a sign for an operation which changed the workspace.
14+
/// This is `false` if the to be applied branch was already contained in the current workspace.
15+
pub fn workspace_changed(&self) -> bool {
16+
matches!(self.graph, Cow::Owned(_))
17+
}
18+
}
19+
20+
impl std::fmt::Debug for Outcome<'_> {
21+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22+
f.debug_struct("Outcome")
23+
.field("workspace_changed", &self.workspace_changed())
24+
.field("workspace_ref_created", &self.workspace_ref_created)
25+
.finish()
26+
}
27+
}
28+
29+
/// How the newly applied branch should be integrated into the workspace.
30+
#[derive(Default, Debug, Copy, Clone)]
31+
pub enum IntegrationMode {
32+
/// Do nothing but to merge it into the workspace commit.
33+
#[default]
34+
Merge,
35+
}
36+
37+
/// What to do if the applied branch conflicts?
38+
#[derive(Default, Debug, Copy, Clone)]
39+
pub enum OnWorkspaceConflict {
40+
/// Provide additional information about the stack that conflicted and the files involved in it,
41+
/// and don't perform the operation.
42+
#[default]
43+
AbortAndReportConflictingStack,
44+
}
45+
46+
/// Decide how a newly created workspace reference should be named.
47+
#[derive(Default, Debug, Clone)]
48+
pub enum WorkspaceReferenceNaming {
49+
/// Create a default workspace branch
50+
#[default]
51+
Default,
52+
/// Create a workspace with the given name instead.
53+
Given(gix::refs::FullName),
54+
}
55+
56+
/// Options for [function::apply()].
57+
#[derive(Default, Debug, Clone)]
58+
pub struct Options {
59+
/// how the branch should be brought into the workspace.
60+
pub integration_mode: IntegrationMode,
61+
/// Decide how to deal with conflicts.
62+
pub on_workspace_conflict: OnWorkspaceConflict,
63+
/// How the workspace reference should be named should it be created.
64+
/// The creation is always needed if there are more than one branch applied.
65+
pub workspace_reference_naming: WorkspaceReferenceNaming,
66+
}
67+
68+
pub(crate) mod function {
69+
use super::{Options, Outcome, WorkspaceReferenceNaming};
70+
use crate::ref_info::WorkspaceExt;
71+
use anyhow::{Context, bail};
72+
use but_core::RefMetadata;
73+
use but_graph::init::Overlay;
74+
use but_graph::projection::WorkspaceKind;
75+
use std::borrow::Cow;
76+
77+
/// Apply `branch` to the given `workspace`, and possibly create the workspace reference in `repo`
78+
/// along with its `meta`-data if it doesn't exist yet.
79+
/// Otherwise, add it to the existing `workspace`, and update its metadata accordingly.
80+
/// **This means that the contents of `branch` is observable from the new state of `repo`**.
81+
///
82+
/// Note that `workspace` is expected to match the state in `repo` as it's used instead of querying `repo` directly
83+
/// where possible.
84+
///
85+
/// Also note that we will create a managed workspace reference as needed if necessary, and a workspace commit if there is more than
86+
/// one reference in the workspace afterward.
87+
///
88+
/// On `error`, neither `repo` nor `meta` will have been changed, but `repo` may contain in-memory objects.
89+
/// Otherwise, objects will have been persisted, and references and metadata will have been updated.
90+
pub fn apply<'graph, T: RefMetadata>(
91+
branch: &gix::refs::FullNameRef,
92+
workspace: &but_graph::projection::Workspace<'graph>,
93+
repo: &mut gix::Repository,
94+
meta: &mut T,
95+
Options {
96+
integration_mode: _,
97+
on_workspace_conflict: _,
98+
workspace_reference_naming,
99+
}: Options,
100+
) -> anyhow::Result<Outcome<'graph>> {
101+
if workspace.refname_is_segment(branch) {
102+
return Ok(Outcome {
103+
graph: Cow::Borrowed(workspace.graph),
104+
workspace_ref_created: false,
105+
});
106+
}
107+
108+
if let Some(ws_ref_name) = workspace.ref_name() {
109+
if repo.try_find_reference(ws_ref_name)?.is_none() {
110+
// The workspace is the probably ad-hoc, and doesn't exist, *assume* unborn.
111+
bail!(
112+
"Cannot create reference on unborn branch '{}'",
113+
ws_ref_name.shorten()
114+
);
115+
}
116+
}
117+
118+
if workspace.has_workspace_commit_in_ancestry(repo) {
119+
bail!("Refusing to work on workspace whose workspace commit isn't at the top");
120+
}
121+
let (workspace_ref_name, must_create_ws_ref) = match &workspace.kind {
122+
WorkspaceKind::Managed { ref_name }
123+
| WorkspaceKind::ManagedMissingWorkspaceCommit { ref_name } => {
124+
(ref_name.clone(), false)
125+
}
126+
WorkspaceKind::AdHoc => {
127+
// We need to switch over to a possibly existing workspace.
128+
let next_ws_ref_name = match workspace_reference_naming {
129+
WorkspaceReferenceNaming::Default => {
130+
gix::refs::FullName::try_from("refs/heads/gitbutler/workspace")
131+
.expect("known statically")
132+
}
133+
WorkspaceReferenceNaming::Given(name) => name,
134+
};
135+
let ws_ref_id = match repo.try_find_reference(next_ws_ref_name.as_ref())? {
136+
None => {
137+
// Create a workspace reference later at the current AdHoc workspace id
138+
workspace
139+
.stacks
140+
.first()
141+
.and_then(|s| s.segments.first())
142+
.and_then(|s| s.commits.first().map(|c| c.id))
143+
.context("BUG: how can an empty ad-hoc workspace exist? Should have at least one stack-segment with commit")?
144+
}
145+
Some(mut existing_workspace_reference) => {
146+
let id = existing_workspace_reference.peel_to_id_in_place()?;
147+
id.detach()
148+
}
149+
};
150+
151+
let graph = workspace.graph.redo_traversal_with_overlay(
152+
repo,
153+
meta,
154+
Overlay::default().with_entrypoint(ws_ref_id, Some(next_ws_ref_name)),
155+
)?;
156+
todo!("put current ad-hoc stack into ")
157+
}
158+
};
159+
160+
// Everything worked? Assure the ref exists now that (nearly nothing) can go wrong anymore.
161+
if must_create_ws_ref {
162+
todo!("create ref")
163+
}
164+
todo!("assure workspace commit is present (always for now), and set everything up");
165+
}
166+
}

crates/but-workspace/src/branch/create_reference.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,14 @@ pub(super) mod function {
218218
ref_name.shorten()
219219
);
220220
};
221-
position.resolve_commit(segment.commits.first().context(
222-
"BUG: empty segments aren't possible without workspace metadata",
223-
)?.into(), ws_base)?
221+
position.resolve_commit(
222+
segment
223+
.commits
224+
.first()
225+
.context("Cannot create reference on unborn branch")?
226+
.into(),
227+
ws_base,
228+
)?
224229
};
225230
(
226231
validate_id,

crates/but-workspace/src/branch/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,10 @@ impl Stack {
456456
}
457457
}
458458

459+
/// Functions and types related to applying a workspace branch.
460+
pub mod apply;
461+
pub use apply::function::apply;
462+
459463
/// related types for removing a workspace reference.
460464
pub mod remove_reference;
461465
pub use remove_reference::function::remove_reference;

0 commit comments

Comments
 (0)