Skip to content

Commit fbce44f

Browse files
committed
SQUASH ME
1 parent 5abc20c commit fbce44f

File tree

4 files changed

+326
-82
lines changed

4 files changed

+326
-82
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,12 @@ impl Graph {
473473
.iter()
474474
.flat_map(|s| s.commits_by_segment.iter().map(|(sidx, _)| *sidx))
475475
}) {
476-
let s = &self.inner[sidx];
476+
// The workspace might be stale by now as we delete empty segments.
477+
// Thus be careful, and ignore non-existing ones - after all our workspace
478+
// is temporary, nothing to worry about.
479+
let Some(s) = self.inner.node_weight(sidx) else {
480+
continue;
481+
};
477482
if s.ref_name.is_some() || s.sibling_segment_id.is_some() {
478483
continue;
479484
}
@@ -702,6 +707,9 @@ fn find_all_desired_stack_refs_in_commit<'a>(
702707
})
703708
}
704709

710+
/// **Warning**: this can make workspace stacks stale, i.e. let them refer to non-existing segments.
711+
/// all accesses from hereon must be done with care. On the other hand, we can ignore
712+
/// that as our workspace is just temporary.
705713
fn delete_anon_if_empty_and_reconnect(graph: &mut Graph, sidx: SegmentIndex) {
706714
let segment = &graph[sidx];
707715
let may_delete = segment.commits.is_empty() && segment.ref_name.is_none();
@@ -717,6 +725,11 @@ fn delete_anon_if_empty_and_reconnect(graph: &mut Graph, sidx: SegmentIndex) {
717725
if outgoing.next().is_some() {
718726
return;
719727
}
728+
729+
tracing::debug!(
730+
?sidx,
731+
"Deleting seemingly isolated and now completely unused segment"
732+
);
720733
// Reconnect
721734
let new_target = first_outgoing.target();
722735
let incoming: Vec<_> = graph

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

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
projection::{Stack, StackCommit, StackCommitFlags, StackSegment},
1111
};
1212
use anyhow::Context;
13-
use bstr::ByteSlice;
13+
use bstr::{BStr, ByteSlice};
1414
use but_core::ref_metadata;
1515
use but_core::ref_metadata::StackId;
1616
use gix::reference::Category;
@@ -165,20 +165,6 @@ impl Workspace<'_> {
165165
}
166166
}
167167

168-
/// Utilities
169-
impl Workspace<'_> {
170-
/// Reconcile the metadata in `ws` to look like this instance.
171-
///
172-
/// This is useful to assure that a possibly [simulated](Graph::redo_traversal_with_overlay()) workspace
173-
/// *(that is soon to be reality)*
174-
/// is going to be stored as-is in metadata.
175-
pub fn update_metadata(&self, ws: &mut ref_metadata::Workspace) {
176-
self.stacks[0].id
177-
ws.stacks
178-
todo!()
179-
}
180-
}
181-
182168
/// A classifier for the workspace.
183169
#[derive(Debug, Clone)]
184170
pub enum WorkspaceKind {
@@ -995,6 +981,12 @@ impl<'graph> Workspace<'graph> {
995981
pub fn ref_name(&self) -> Option<&'graph gix::refs::FullNameRef> {
996982
self.graph[self.id].ref_name.as_ref().map(|rn| rn.as_ref())
997983
}
984+
985+
/// Like [`Self::ref_name()`], but return a generic `<anonymous>` name for unnamed workspaces.
986+
pub fn ref_name_display(&self) -> &BStr {
987+
self.ref_name()
988+
.map_or("<anonymous>".into(), |rn| rn.as_bstr())
989+
}
998990
}
999991

1000992
/// Debugging

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

Lines changed: 151 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,13 @@ impl<'a> ReferenceAnchor<'a> {
103103

104104
pub(super) mod function {
105105
#![allow(clippy::indexing_slicing)]
106+
106107
use crate::branch::{ReferenceAnchor, ReferencePosition};
107108
use anyhow::{Context, bail};
108109
use but_core::ref_metadata::{StackId, WorkspaceStack, WorkspaceStackBranch};
109110
use but_core::{RefMetadata, ref_metadata};
110111
use gix::refs::transaction::PreviousValue;
112+
use std::borrow::Cow;
111113
use std::ops::DerefMut;
112114

113115
/// Create a new reference named `ref_name` to point at a commit relative to `anchor`.
@@ -132,7 +134,17 @@ pub(super) mod function {
132134
let anchor = anchor.into();
133135

134136
let ws_base = workspace.lower_bound;
135-
let (check_if_commit_in_workspace, ref_target, insert): (_, _, Option<Instruction<'_>>) = {
137+
// Note that we will never create metadata for a workspace!
138+
let mut existing_ws_meta = workspace
139+
.ref_name()
140+
.and_then(|ws_ref| meta.workspace_opt(ws_ref).transpose())
141+
.transpose()?;
142+
143+
let (check_if_id_in_workspace, ref_target_id, instruction): (
144+
_,
145+
_,
146+
Option<Instruction<'_>>,
147+
) = {
136148
match anchor {
137149
None => {
138150
// The new ref exists already in the workspace, do nothing.
@@ -145,40 +157,47 @@ pub(super) mod function {
145157
let base = ws_base.with_context(|| {
146158
format!(
147159
"workspace at {} is missing a base",
148-
workspace.ref_name().map_or_else(
149-
|| "<anonymous>".to_string(),
150-
|rn| rn.as_bstr().to_string()
151-
)
160+
workspace.ref_name_display()
152161
)
153162
})?;
154163
(
155164
// do not validate, as the base is expectedly outside of workspace
156165
false,
157166
base,
158-
Some(None),
167+
Some(Instruction::Independent),
159168
)
160169
}
161170
Some(ReferenceAnchor::AtCommit {
162171
commit_id,
163172
position,
164173
}) => {
165174
let mut validate_id = true;
166-
let id = position.resolve_commit(
167-
workspace
168-
.lookup_commit(
169-
workspace.try_find_owner_indexes_by_commit_id(commit_id)?,
170-
)
171-
.into(),
172-
ws_base,
173-
)?;
174-
if Some(id) == ws_base {
175+
let indexes = workspace.try_find_owner_indexes_by_commit_id(commit_id)?;
176+
let ref_target_id = position
177+
.resolve_commit(workspace.lookup_commit(indexes).into(), ws_base)?;
178+
let id_out_of_workspace = Some(ref_target_id) == ws_base;
179+
if id_out_of_workspace {
175180
validate_id = false
176181
}
177-
(validate_id, id, None)
182+
183+
let instruction = existing_ws_meta
184+
.as_ref()
185+
.filter(|_| !id_out_of_workspace)
186+
.map(|_| {
187+
instruction_by_named_anchor_for_commit(
188+
workspace,
189+
commit_id,
190+
ref_target_id,
191+
position,
192+
)
193+
})
194+
.transpose()?;
195+
196+
(validate_id, ref_target_id, instruction)
178197
}
179198
Some(ReferenceAnchor::AtSegment { ref_name, position }) => {
180199
let mut validate_id = true;
181-
let id = if workspace.has_metadata() {
200+
let ref_target_id = if workspace.has_metadata() {
182201
let (stack_idx, seg_idx) =
183202
workspace.try_find_segment_owner_indexes_by_refname(ref_name)?;
184203
let segment = &workspace.stacks[stack_idx].segments[seg_idx];
@@ -207,29 +226,28 @@ pub(super) mod function {
207226
"BUG: empty segments aren't possible without workspace metadata",
208227
)?.into(), ws_base)?
209228
};
210-
(validate_id, id, Some(Some((ref_name, position))))
229+
(
230+
validate_id,
231+
ref_target_id,
232+
Some(Instruction::Dependent {
233+
ref_name: Cow::Borrowed(ref_name),
234+
position,
235+
}),
236+
)
211237
}
212238
}
213239
};
214240

215-
let mut existing_ws_meta = workspace
216-
.ref_name()
217-
.and_then(|ws_ref| meta.workspace_opt(ws_ref).transpose())
241+
let updated_ws_meta = existing_ws_meta
242+
.take()
243+
.zip(instruction)
244+
.map(|(mut existing, instruction)| {
245+
update_workspace_metadata(&mut existing, ref_name, instruction).map(|()| existing)
246+
})
218247
.transpose()?;
219-
let updated_ws_meta = if let Some(instruction) = insert {
220-
if let Some(mut existing) = existing_ws_meta.take() {
221-
update_workspace_metadata(&mut existing, ref_name, instruction)?;
222-
Some(existing)
223-
} else {
224-
None
225-
}
226-
} else {
227-
None
228-
};
229-
230248
// Assure this commit is in the workspace as well.
231-
if check_if_commit_in_workspace {
232-
workspace.try_find_owner_indexes_by_commit_id(ref_target)?;
249+
if check_if_id_in_workspace {
250+
workspace.try_find_owner_indexes_by_commit_id(ref_target_id)?;
233251
}
234252

235253
let graph_with_new_ref = {
@@ -243,7 +261,7 @@ pub(super) mod function {
243261
but_graph::init::Overlay::default()
244262
.with_references_if_new(Some(gix::refs::Reference {
245263
name: ref_name.into(),
246-
target: gix::refs::Target::Object(ref_target),
264+
target: gix::refs::Target::Object(ref_target_id),
247265
peeled: None,
248266
}))
249267
.with_branch_metadata_override(Some((
@@ -265,24 +283,23 @@ pub(super) mod function {
265283
if !has_new_ref_as_standalone_segment {
266284
// TODO: this should probably be easier to understand for the UI, with error codes maybe?
267285
bail!(
268-
"Reference '{}' cannot be created as segment at {ref_target}",
286+
"Reference '{}' cannot be created as segment at {ref_target_id}",
269287
ref_name.shorten()
270288
)
271289
}
272290

273291
// Actually apply the changes
274292
repo.reference(
275293
ref_name,
276-
ref_target,
277-
PreviousValue::ExistingMustMatch(gix::refs::Target::Object(ref_target)),
294+
ref_target_id,
295+
PreviousValue::ExistingMustMatch(gix::refs::Target::Object(ref_target_id)),
278296
"Dependent branch by GitButler",
279297
)?;
280298
// Important to first update the workspace so we have the correct stack setup.
281299
if let Some(ws_meta) = updated_ws_meta {
282-
// TODO: overwrite stored information with reality in new graph.
283300
meta.set_workspace(&ws_meta)?;
284-
} else if let Some(mut existing) = existing_ws_meta {
285-
updated_workspace.update_metadata(&mut *existing);
301+
} else if let Some(existing) = existing_ws_meta {
302+
// TODO: overwrite stored information with reality in new graph.
286303
meta.set_workspace(&existing)?;
287304
}
288305

@@ -315,23 +332,25 @@ pub(super) mod function {
315332
new_ref: &gix::refs::FullNameRef,
316333
instruction: Instruction<'_>,
317334
) -> anyhow::Result<()> {
335+
if ws_meta.find_branch(new_ref).is_some() {
336+
return Ok(());
337+
}
318338
match instruction {
319339
// create new
320-
None => {
321-
if ws_meta.find_branch(new_ref).is_none() {
322-
ws_meta.stacks.push(WorkspaceStack {
323-
id: StackId::generate(),
324-
branches: vec![WorkspaceStackBranch {
325-
ref_name: new_ref.to_owned(),
326-
archived: false,
327-
}],
328-
})
329-
}
330-
}
340+
Instruction::Independent => ws_meta.stacks.push(WorkspaceStack {
341+
id: StackId::generate(),
342+
branches: vec![WorkspaceStackBranch {
343+
ref_name: new_ref.to_owned(),
344+
archived: false,
345+
}],
346+
}),
331347
// insert dependent branch at anchor
332-
Some((anchor_ref, position)) => {
348+
Instruction::Dependent {
349+
ref_name: anchor_ref,
350+
position,
351+
} => {
333352
let (stack_idx, branch_idx) = ws_meta
334-
.find_owner_indexes_by_name(anchor_ref)
353+
.find_owner_indexes_by_name(anchor_ref.as_ref())
335354
.with_context(|| {
336355
format!(
337356
"Couldn't find anchor '{}' in workspace metadata",
@@ -354,5 +373,82 @@ pub(super) mod function {
354373
Ok(())
355374
}
356375

357-
type Instruction<'a> = Option<(&'a gix::refs::FullNameRef, ReferencePosition)>;
376+
/// Create the instruction that would be needed to insert the new ref-name into workspace data
377+
/// so that it represents the `position` of `anchor_id` and `target_id` (being (`anchor_id` offset by `position`).
378+
/// `position` indicates where, in relation to `anchor_id`, the ref name should be inserted.
379+
/// The first name that is also in `ws_meta` will be used.
380+
fn instruction_by_named_anchor_for_commit(
381+
ws: &but_graph::projection::Workspace<'_>,
382+
anchor_id: gix::ObjectId,
383+
target_id: gix::ObjectId,
384+
position: ReferencePosition,
385+
) -> anyhow::Result<Instruction<'static>> {
386+
use ReferencePosition::*;
387+
let (anchor_stack_idx, anchor_seg_idx, _anchor_commit_idx) = ws
388+
.find_owner_indexes_by_commit_id(anchor_id)
389+
.with_context(|| {
390+
format!(
391+
"No segment in workspace at '{}' that holds {anchor_id}",
392+
ws.ref_name_display()
393+
)
394+
})?;
395+
let (target_stack_idx, target_seg_idx, _target_commit_idx) = ws
396+
.find_owner_indexes_by_commit_id(target_id)
397+
.with_context(|| {
398+
format!(
399+
"No segment in workspace at '{}' that holds {target_id}",
400+
ws.ref_name_display()
401+
)
402+
})?;
403+
if anchor_stack_idx != target_stack_idx {
404+
bail!(
405+
"BUG: Anchor id {anchor_id} and {target_id} ended up in different stacks - how can that be?"
406+
);
407+
}
408+
409+
// TODO: this is missing a lot of tests for the empty-segment complexity, but also the
410+
let stack = &ws.stacks[anchor_stack_idx];
411+
if anchor_seg_idx == target_seg_idx {
412+
// Find first non-empty segment in this stack upward and downward.
413+
(0..anchor_seg_idx + 1).rev()
414+
.find_map(|seg_idx| {
415+
stack.segments[seg_idx]
416+
.ref_name
417+
.as_ref()
418+
.map(|rn| (rn.as_ref(), Below))
419+
})
420+
.or_else(|| {
421+
(anchor_seg_idx + 1..stack.segments.len()).find_map(|seg_idx| {
422+
stack.segments[seg_idx]
423+
.ref_name
424+
.as_ref()
425+
.map(|rn| (rn.as_ref(), Above))
426+
})
427+
})
428+
.map(|(anchor_ref, position)| Instruction::Dependent {
429+
ref_name: Cow::Owned(anchor_ref.to_owned()),
430+
position,
431+
})
432+
} else {
433+
if anchor_seg_idx + 1 != target_seg_idx {
434+
bail!(
435+
"BUG: The anchor commit {anchor_id} must be above {target_id}, but the segments where {anchor_seg_idx} and {target_seg_idx}"
436+
);
437+
}
438+
todo!("sort out start position and positioning")
439+
}.with_context(|| {
440+
format!(
441+
"Didn't find a named segment in workspace at '{}' to indicate where to insert {anchor_id} {position:?}",
442+
ws.ref_name_display()
443+
)
444+
})
445+
}
446+
447+
enum Instruction<'a> {
448+
Independent,
449+
Dependent {
450+
ref_name: Cow<'a, gix::refs::FullNameRef>,
451+
position: ReferencePosition,
452+
},
453+
}
358454
}

0 commit comments

Comments
 (0)