Skip to content

Commit 1eb8d82

Browse files
committed
Store index data in the snapshot, and resolve it as well.
1 parent 1adde67 commit 1eb8d82

File tree

11 files changed

+501
-66
lines changed

11 files changed

+501
-66
lines changed

crates/but-core/src/diff/mod.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ use bstr::{BStr, ByteSlice};
44
pub use tree_changes::tree_changes;
55

66
mod worktree;
7-
use crate::{ChangeState, ModeFlags, TreeChange, TreeStatus, TreeStatusKind};
8-
pub use worktree::worktree_changes;
7+
use crate::{
8+
ChangeState, IgnoredWorktreeChange, ModeFlags, TreeChange, TreeStatus, TreeStatusKind,
9+
};
10+
pub use worktree::{worktree_changes, worktree_changes_no_renames};
911

1012
/// conversion functions for use in the UI
1113
pub mod ui;
@@ -68,6 +70,15 @@ impl std::fmt::Debug for TreeChange {
6870
}
6971
}
7072

73+
impl std::fmt::Debug for IgnoredWorktreeChange {
74+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75+
f.debug_struct("IgnoredWorktreeChange")
76+
.field("path", &self.path)
77+
.field("status", &self.status)
78+
.finish()
79+
}
80+
}
81+
7182
impl ModeFlags {
7283
fn calculate(old: &ChangeState, new: &ChangeState) -> Option<Self> {
7384
Self::calculate_inner(old.kind, new.kind)

crates/but-core/src/diff/worktree.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,44 @@ enum Origin {
3333
/// to get a commit with a tree equal to the current worktree.
3434
#[instrument(skip(repo), err(Debug))]
3535
pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result<WorktreeChanges> {
36-
let rewrites = gix::diff::Rewrites::default(); /* standard Git rewrite handling for everything */
37-
debug_assert!(
38-
rewrites.copies.is_none(),
39-
"TODO: copy tracking needs specific support wherever 'previous_path()' is called."
40-
);
36+
worktree_changes_inner(repo, RenameTracking::Always)
37+
}
38+
39+
/// Just like [`worktree_changes()`], but don't do any rename tracking for performance.
40+
#[instrument(skip(repo), err(Debug))]
41+
pub fn worktree_changes_no_renames(repo: &gix::Repository) -> anyhow::Result<WorktreeChanges> {
42+
worktree_changes_inner(repo, RenameTracking::Disabled)
43+
}
44+
45+
enum RenameTracking {
46+
Always,
47+
Disabled,
48+
}
49+
50+
fn worktree_changes_inner(
51+
repo: &gix::Repository,
52+
renames: RenameTracking,
53+
) -> anyhow::Result<WorktreeChanges> {
54+
let (tree_index_rewrites, worktree_rewrites) = match renames {
55+
RenameTracking::Always => {
56+
let rewrites = gix::diff::Rewrites::default(); /* standard Git rewrite handling for everything */
57+
debug_assert!(
58+
rewrites.copies.is_none(),
59+
"TODO: copy tracking needs specific support wherever 'previous_path()' is called."
60+
);
61+
(TrackRenames::Given(rewrites), Some(rewrites))
62+
}
63+
RenameTracking::Disabled => (TrackRenames::Disabled, None),
64+
};
4165
let has_submodule_ignore_configuration = repo.modules()?.is_some_and(|modules| {
4266
modules
4367
.names()
4468
.any(|name| modules.ignore(name).ok().flatten().is_some())
4569
});
4670
let status_changes = repo
4771
.status(gix::progress::Discard)?
48-
.tree_index_track_renames(TrackRenames::Given(rewrites))
49-
.index_worktree_rewrites(rewrites)
72+
.tree_index_track_renames(tree_index_rewrites)
73+
.index_worktree_rewrites(worktree_rewrites)
5074
// Learn about submodule changes, but only do the cheap checks, showing only what we could commit.
5175
.index_worktree_submodules(if has_submodule_ignore_configuration {
5276
gix::status::Submodule::AsConfigured { check_dirty: true }
@@ -415,6 +439,7 @@ pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result<WorktreeChange
415439
ignored_changes.push(IgnoredWorktreeChange {
416440
path: rela_path,
417441
status: IgnoredWorktreeTreeChangeStatus::Conflict,
442+
status_item: Some(change),
418443
});
419444
continue;
420445
}
@@ -483,15 +508,17 @@ pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result<WorktreeChange
483508
changes.push(merged);
484509
IgnoredWorktreeTreeChangeStatus::TreeIndex
485510
}
486-
[Some(first), Some(second)] => {
511+
[Some(mut first), Some(mut second)] => {
487512
ignored_changes.push(IgnoredWorktreeChange {
488513
path: first.path.clone(),
489514
status: IgnoredWorktreeTreeChangeStatus::TreeIndex,
515+
status_item: first.status_item.take(),
490516
});
491517
changes.push(first);
492518
ignored_changes.push(IgnoredWorktreeChange {
493519
path: second.path.clone(),
494520
status: IgnoredWorktreeTreeChangeStatus::TreeIndex,
521+
status_item: second.status_item.take(),
495522
});
496523
changes.push(second);
497524
continue;
@@ -500,6 +527,7 @@ pub fn worktree_changes(repo: &gix::Repository) -> anyhow::Result<WorktreeChange
500527
ignored_changes.push(IgnoredWorktreeChange {
501528
path: change_path,
502529
status,
530+
status_item: None,
503531
});
504532
continue;
505533
}

crates/but-core/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,13 +332,17 @@ pub enum IgnoredWorktreeTreeChangeStatus {
332332
}
333333

334334
/// A way to indicate that a path in the index isn't suitable for committing and needs to be dealt with.
335-
#[derive(Debug, Clone, Serialize)]
335+
#[derive(Clone, Serialize)]
336336
pub struct IgnoredWorktreeChange {
337337
/// The worktree-relative path to the change.
338338
#[serde(serialize_with = "gitbutler_serde::bstring_lossy::serialize")]
339339
pub path: BString,
340340
/// The status that caused this change to be ignored.
341341
pub status: IgnoredWorktreeTreeChangeStatus,
342+
/// The status item that this change is derived from, used in places that need detailed information.
343+
/// It's `None` if the status item is already present in non-ignored changes.
344+
#[serde(skip)]
345+
pub status_item: Option<gix::status::Item>,
342346
}
343347

344348
/// The type returned by [`worktree_changes()`](diff::worktree_changes).

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ impl<'a> ReferenceAnchor<'a> {
106106
}
107107

108108
pub(super) mod function {
109-
#![allow(clippy::indexing_slicing)]
109+
#![expect(clippy::indexing_slicing)]
110110

111111
use crate::branch::{ReferenceAnchor, ReferencePosition};
112112
use anyhow::{Context, bail};

crates/but-workspace/src/ref_info.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![allow(clippy::indexing_slicing)]
1+
#![expect(clippy::indexing_slicing)]
22
// TODO: rename this module to `workspace`, make it private, and pub-use all content in the top-level, as we now literally
33
// get the workspace, while possibly processing it for use in the UI.
44

crates/but-workspace/src/snapshot/create_tree.rs

Lines changed: 170 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::collections::BTreeSet;
55
/// A way to determine what should be included in the snapshot when calling [create_tree()](function::create_tree).
66
#[derive(Debug, Clone)]
77
pub struct State {
8-
/// The result of a previous worktree changes call.
8+
/// The result of a previous worktree changes call, but [the one **without** renames](but_core::diff::worktree_changes_no_renames()).
99
///
1010
/// It contains detailed information about the complete set of possible changes to become part of the worktree.
1111
pub changes: but_core::WorktreeChanges,
@@ -59,9 +59,13 @@ pub fn no_workspace_and_meta() -> Option<(
5959
pub(super) mod function {
6060
use super::{Outcome, State};
6161
use crate::{DiffSpec, commit_engine};
62-
use anyhow::bail;
63-
use but_core::RefMetadata;
62+
use anyhow::{Context, bail};
63+
use bstr::{BString, ByteSlice};
64+
use but_core::{ChangeState, RefMetadata};
65+
use gix::diff::index::Change;
6466
use gix::object::tree::EntryKind;
67+
use gix::status::plumbing::index_as_worktree::EntryStatus;
68+
use std::collections::BTreeSet;
6569
use tracing::instrument;
6670

6771
/// Create a tree that represents the snapshot for the given `selection`, whereas the basis for these changes
@@ -94,7 +98,10 @@ pub(super) mod function {
9498
/// even though it might be that the respective objects aren't written to disk yet.
9599
/// - Note that this tree may contain files with conflict markers as it will pick up the conflicting state visible on disk.
96100
/// * `index`
97-
/// - A representation of the non-conflicting portions of the index, without its meta-data.
101+
/// - A representation of the non-conflicting and changed portions of the index, without its meta-data.
102+
/// - may be empty if only conflicts exist.
103+
/// * `index-conflicts`
104+
/// - `<entry-path>/[1,2,3]` - the blobs at their respective stages.
98105
#[instrument(skip(changes, _workspace_and_meta), err(Debug))]
99106
pub fn create_tree(
100107
head_tree_id: gix::Id<'_>,
@@ -105,13 +112,47 @@ pub(super) mod function {
105112
}: State,
106113
_workspace_and_meta: Option<(&but_graph::projection::Workspace, &impl RefMetadata)>,
107114
) -> anyhow::Result<Outcome> {
115+
// Assure this is a tree early.
116+
let head_tree = head_tree_id.object()?.into_tree();
108117
let repo = head_tree_id.repo;
109118
let mut changes_to_apply: Vec<_> = changes
110119
.changes
111120
.iter()
112121
.filter(|c| selection.contains(&c.path))
113122
.map(|c| Ok(DiffSpec::from(c)))
114123
.collect();
124+
changes_to_apply.extend(
125+
changes
126+
.ignored_changes
127+
.iter()
128+
.filter_map(|c| match &c.status_item {
129+
Some(gix::status::Item::IndexWorktree(
130+
gix::status::index_worktree::Item::Modification {
131+
status: EntryStatus::Conflict { .. },
132+
rela_path,
133+
..
134+
},
135+
)) => Some(rela_path),
136+
_ => None,
137+
})
138+
.filter(|rela_path| selection.contains(rela_path.as_bstr()))
139+
.map(|rela_path| {
140+
// Create a pretend-addition to pick up conflicted paths as well.
141+
Ok(DiffSpec::from(but_core::TreeChange {
142+
path: rela_path.to_owned(),
143+
status: but_core::TreeStatus::Addition {
144+
state: ChangeState {
145+
id: repo.object_hash().null(),
146+
// This field isn't relevant when entries are read from disk.
147+
kind: EntryKind::Tree,
148+
},
149+
is_untracked: true,
150+
},
151+
status_item: None,
152+
}))
153+
}),
154+
);
155+
115156
let (new_tree, base_tree) = commit_engine::tree::apply_worktree_changes(
116157
head_tree_id.into(),
117158
repo,
@@ -138,19 +179,141 @@ pub(super) mod function {
138179
needs_head = true;
139180
}
140181

182+
let (index, index_conflicts) = snapshot_index(&mut edit, head_tree, changes, selection)?
183+
.inspect(|(index, index_conflicts)| {
184+
needs_head |= index_conflicts.is_some() && index.is_none();
185+
})
186+
.unwrap_or_default();
187+
141188
if needs_head {
142189
edit.upsert("HEAD", EntryKind::Tree, head_tree_id)?;
143190
}
144191

145192
Ok(Outcome {
146193
snapshot_tree: edit.write()?.into(),
147-
head_tree: head_tree_id.into(),
194+
head_tree: head_tree_id.detach(),
148195
worktree,
149-
index: None,
150-
index_conflicts: None,
196+
index,
197+
index_conflicts,
151198
workspace_references: None,
152199
head_references: None,
153200
metadata: None,
154201
})
155202
}
203+
204+
/// `snapshot_tree` is the tree into which our `index` and `index-conflicts` trees are written. These will also be returned
205+
/// if they were written.
206+
///
207+
/// `base_tree_id` is the tree from which a clean index can be created, and which we will edit to incorporate the
208+
/// non-conflicting index changes.
209+
fn snapshot_index(
210+
snapshot_tree: &mut gix::object::tree::Editor,
211+
base_tree: gix::Tree,
212+
changes: but_core::WorktreeChanges,
213+
selection: BTreeSet<BString>,
214+
) -> anyhow::Result<Option<(Option<gix::ObjectId>, Option<gix::ObjectId>)>> {
215+
let mut conflicts = Vec::new();
216+
let changes: Vec<_> = changes
217+
.changes
218+
.into_iter()
219+
.filter_map(|c| c.status_item)
220+
.chain(
221+
changes
222+
.ignored_changes
223+
.into_iter()
224+
.filter_map(|c| c.status_item),
225+
)
226+
.filter_map(|item| match item {
227+
gix::status::Item::IndexWorktree(
228+
gix::status::index_worktree::Item::Modification {
229+
status: EntryStatus::Conflict { entries, .. },
230+
rela_path,
231+
..
232+
},
233+
) => {
234+
conflicts.push((rela_path, entries));
235+
None
236+
}
237+
gix::status::Item::TreeIndex(c) => Some(c),
238+
_ => None,
239+
})
240+
.filter(|c| selection.iter().any(|path| path == c.location()))
241+
.collect();
242+
243+
if changes.is_empty() && conflicts.is_empty() {
244+
return Ok(None);
245+
}
246+
247+
let mut base_tree_edit = base_tree.edit()?;
248+
for change in changes {
249+
match change {
250+
Change::Deletion { location, .. } => {
251+
base_tree_edit.remove(location.as_bstr())?;
252+
}
253+
Change::Addition {
254+
location,
255+
entry_mode,
256+
id,
257+
..
258+
}
259+
| Change::Modification {
260+
location,
261+
entry_mode,
262+
id,
263+
..
264+
} => {
265+
base_tree_edit.upsert(
266+
location.as_bstr(),
267+
entry_mode
268+
.to_tree_entry_mode()
269+
.with_context(|| format!("Could not convert the index entry {entry_mode:?} at '{location}' into a tree entry kind"))?
270+
.kind(),
271+
id.into_owned(),
272+
)?;
273+
}
274+
Change::Rewrite { .. } => {
275+
unreachable!("BUG: this must have been deactivated")
276+
}
277+
}
278+
}
279+
280+
let index = base_tree_edit.write()?;
281+
let index = (index != base_tree.id).then_some(index.detach());
282+
if let Some(index) = index {
283+
snapshot_tree.upsert("index", EntryKind::Tree, index)?;
284+
}
285+
286+
let index_conflicts = if conflicts.is_empty() {
287+
None
288+
} else {
289+
let mut root = snapshot_tree.cursor_at("index-conflicts")?;
290+
for (rela_path, conflict_entries) in conflicts {
291+
for (stage, entry) in conflict_entries
292+
.into_iter()
293+
.enumerate()
294+
.filter_map(|(idx, e)| e.map(|e| (idx + 1, e)))
295+
{
296+
root.upsert(
297+
format!("{rela_path}/{stage}"),
298+
entry
299+
.mode
300+
.to_tree_entry_mode()
301+
.with_context(|| {
302+
format!(
303+
"Could not convert the index entry {entry_mode:?} \
304+
at '{location}' into a tree entry kind",
305+
entry_mode = entry.mode,
306+
location = rela_path
307+
)
308+
})?
309+
.kind(),
310+
entry.id,
311+
)?;
312+
}
313+
}
314+
root.write()?.detach().into()
315+
};
316+
317+
Ok(Some((index, index_conflicts)))
318+
}
156319
}

0 commit comments

Comments
 (0)