Skip to content

Commit b155ad9

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 9153a1d commit b155ad9

16 files changed

+725
-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: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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, *even* if it's not needed as the workspace reference
33+
/// can connect directly with the *one* workspace base.
34+
#[default]
35+
AlwaysMerge,
36+
/// Only create a merge commit if a new commit is effectively merged in. This avoids *unnecessary* merge commits,
37+
/// but also requires support for this when creating commits (which may then have to create a merge-commit themselves).
38+
// TODO: make this the default
39+
MergeIfNeeded,
40+
}
41+
42+
/// What to do if the applied branch conflicts?
43+
#[derive(Default, Debug, Copy, Clone)]
44+
pub enum OnWorkspaceConflict {
45+
/// Provide additional information about the stack that conflicted and the files involved in it,
46+
/// and don't perform the operation.
47+
#[default]
48+
AbortAndReportConflictingStack,
49+
}
50+
51+
/// Decide how a newly created workspace reference should be named.
52+
#[derive(Default, Debug, Clone)]
53+
pub enum WorkspaceReferenceNaming {
54+
/// Create a default workspace branch
55+
#[default]
56+
Default,
57+
/// Create a workspace with the given name instead.
58+
Given(gix::refs::FullName),
59+
}
60+
61+
/// Options for [function::apply()].
62+
#[derive(Default, Debug, Clone)]
63+
pub struct Options {
64+
/// how the branch should be brought into the workspace.
65+
pub integration_mode: IntegrationMode,
66+
/// Decide how to deal with conflicts.
67+
pub on_workspace_conflict: OnWorkspaceConflict,
68+
/// How the workspace reference should be named should it be created.
69+
/// The creation is always needed if there are more than one branch applied.
70+
pub workspace_reference_naming: WorkspaceReferenceNaming,
71+
}
72+
73+
pub(crate) mod function {
74+
use super::{Options, Outcome, WorkspaceReferenceNaming};
75+
use crate::ref_info::WorkspaceExt;
76+
use anyhow::{Context, bail};
77+
use but_core::ref_metadata::{StackId, ValueInfo, WorkspaceStack, WorkspaceStackBranch};
78+
use but_core::{RefMetadata, ref_metadata};
79+
use but_graph::init::Overlay;
80+
use but_graph::projection::WorkspaceKind;
81+
use std::borrow::Cow;
82+
83+
/// Apply `branch` to the given `workspace`, and possibly create the workspace reference in `repo`
84+
/// along with its `meta`-data if it doesn't exist yet.
85+
/// Otherwise, add it to the existing `workspace`, and update its metadata accordingly.
86+
/// **This means that the contents of `branch` is observable from the new state of `repo`**.
87+
///
88+
/// Note that `workspace` is expected to match the state in `repo` as it's used instead of querying `repo` directly
89+
/// where possible.
90+
///
91+
/// Also note that we will create a managed workspace reference as needed if necessary, and a workspace commit if there is more than
92+
/// one reference in the workspace afterward.
93+
///
94+
/// On `error`, neither `repo` nor `meta` will have been changed, but `repo` may contain in-memory objects.
95+
/// Otherwise, objects will have been persisted, and references and metadata will have been updated.
96+
pub fn apply<'graph, T: RefMetadata>(
97+
branch: &gix::refs::FullNameRef,
98+
workspace: &but_graph::projection::Workspace<'graph>,
99+
repo: &mut gix::Repository,
100+
meta: &mut T,
101+
Options {
102+
integration_mode: _,
103+
on_workspace_conflict: _,
104+
workspace_reference_naming,
105+
}: Options,
106+
) -> anyhow::Result<Outcome<'graph>> {
107+
if workspace.refname_is_segment(branch) {
108+
return Ok(Outcome {
109+
graph: Cow::Borrowed(workspace.graph),
110+
workspace_ref_created: false,
111+
});
112+
}
113+
114+
if let Some(ws_ref_name) = workspace.ref_name() {
115+
if repo.try_find_reference(ws_ref_name)?.is_none() {
116+
// The workspace is the probably ad-hoc, and doesn't exist, *assume* unborn.
117+
bail!(
118+
"Cannot create reference on unborn branch '{}'",
119+
ws_ref_name.shorten()
120+
);
121+
}
122+
}
123+
124+
if workspace.has_workspace_commit_in_ancestry(repo) {
125+
bail!("Refusing to work on workspace whose workspace commit isn't at the top");
126+
}
127+
128+
let (workspace_ref_name_to_update, ws_ref_metadata, branch_to_apply_metadata, graph) =
129+
match &workspace.kind {
130+
WorkspaceKind::Managed { ref_name }
131+
| WorkspaceKind::ManagedMissingWorkspaceCommit { ref_name } => (
132+
ref_name.clone(),
133+
meta.workspace(ref_name.as_ref())?,
134+
None,
135+
Cow::Borrowed(workspace.graph),
136+
),
137+
WorkspaceKind::AdHoc => {
138+
// We need to switch over to a possibly existing workspace.
139+
// We know that the current branch is *not* reachable from the workspace or isn't naturally included,
140+
// so it needs to be added as well.
141+
let next_ws_ref_name = match workspace_reference_naming {
142+
WorkspaceReferenceNaming::Default => {
143+
gix::refs::FullName::try_from("refs/heads/gitbutler/workspace")
144+
.expect("known statically")
145+
}
146+
WorkspaceReferenceNaming::Given(name) => name,
147+
};
148+
let ws_ref_id = match repo.try_find_reference(next_ws_ref_name.as_ref())? {
149+
None => {
150+
// Create a workspace reference later at the current AdHoc workspace id
151+
let ws_id = workspace
152+
.stacks
153+
.first()
154+
.and_then(|s| s.segments.first())
155+
.and_then(|s| s.commits.first().map(|c| c.id))
156+
.context("BUG: how can an empty ad-hoc workspace exist? Should have at least one stack-segment with commit")?;
157+
ws_id
158+
}
159+
Some(mut existing_workspace_reference) => {
160+
let id = existing_workspace_reference.peel_to_id_in_place()?;
161+
id.detach()
162+
}
163+
};
164+
165+
// Get as close as possible to what would happen if the branch was already part of it (without merging),
166+
// and branch-metadata can disambiguate. Having this data also isn't harmful.
167+
// We want to see if the branch is naturally included in workspace already.
168+
let branch_md = meta.branch(branch)?;
169+
let (branch_md_override, branch_md_to_set) = if branch_md.is_default() {
170+
(
171+
Some((branch.to_owned(), (*branch_md).clone())),
172+
Some(branch_md),
173+
)
174+
} else {
175+
(None, None)
176+
};
177+
let mut ws_md = meta.workspace(next_ws_ref_name.as_ref())?;
178+
{
179+
let ws_mut: &mut ref_metadata::Workspace = &mut *ws_md;
180+
ws_mut.stacks.push(WorkspaceStack {
181+
id: StackId::generate(),
182+
branches: vec![WorkspaceStackBranch {
183+
ref_name: branch.to_owned(),
184+
archived: false,
185+
}],
186+
})
187+
}
188+
let ws_md_override = Some((next_ws_ref_name.clone(), (*ws_md).clone()));
189+
190+
let graph = workspace.graph.redo_traversal_with_overlay(
191+
repo,
192+
meta,
193+
Overlay::default()
194+
.with_entrypoint(ws_ref_id, Some(next_ws_ref_name))
195+
.with_branch_metadata_override(branch_md_override)
196+
.with_workspace_metadata_override(ws_md_override),
197+
)?;
198+
199+
let ws = graph.to_workspace()?;
200+
if ws.refname_is_segment(branch) {
201+
todo!(
202+
"no need to add this branch, but we need to add the current ad-hoc branch instead"
203+
);
204+
}
205+
206+
todo!("put current ad-hoc stack into ")
207+
}
208+
};
209+
210+
// Everything worked? Assure the ref exists now that (nearly nothing) can go wrong anymore.
211+
let workspace_ref_created = false; // TODO: use rval of reference update to know if it existed.
212+
213+
if let Some(branch_md) = branch_to_apply_metadata {
214+
meta.set_branch(branch_md)?;
215+
}
216+
217+
Ok(Outcome {
218+
graph,
219+
workspace_ref_created,
220+
})
221+
}
222+
}

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)