Skip to content

Commit 3a62763

Browse files
committed
Store index data in the snapshot, and resolve it as well.
1 parent 14777d4 commit 3a62763

File tree

9 files changed

+357
-45
lines changed

9 files changed

+357
-45
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/snapshot/create_tree.rs

Lines changed: 134 additions & 5 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;
62+
use anyhow::{Context, bail};
63+
use bstr::{BString, ByteSlice};
6364
use but_core::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
@@ -105,6 +109,8 @@ pub(super) mod function {
105109
}: State,
106110
_workspace_and_meta: Option<(&but_graph::projection::Workspace, &impl RefMetadata)>,
107111
) -> anyhow::Result<Outcome> {
112+
// Assure this is a tree.
113+
let head_tree = head_tree_id.object()?.into_tree();
108114
let repo = head_tree_id.repo;
109115
let mut changes_to_apply: Vec<_> = changes
110116
.changes
@@ -138,19 +144,142 @@ pub(super) mod function {
138144
needs_head = true;
139145
}
140146

147+
let (index, index_conflicts) = snapshot_index(&mut edit, head_tree, changes, selection)?
148+
.inspect(|_| {
149+
needs_head = true;
150+
})
151+
.unwrap_or_default();
152+
141153
if needs_head {
142154
edit.upsert("HEAD", EntryKind::Tree, head_tree_id)?;
143155
}
144156

145157
Ok(Outcome {
146158
snapshot_tree: edit.write()?.into(),
147-
head_tree: head_tree_id.into(),
159+
head_tree: head_tree_id.detach(),
148160
worktree,
149-
index: None,
150-
index_conflicts: None,
161+
index,
162+
index_conflicts,
151163
workspace_references: None,
152164
head_references: None,
153165
metadata: None,
154166
})
155167
}
168+
169+
/// `snapshot_tree` is the tree into which our `index` and `index-conflicts` trees are written. These will also be returned
170+
/// if they were written.
171+
///
172+
/// `base_tree_id` is the tree from which a clean index can be created, and which we will edit to incorporate the
173+
/// non-conflicting index changes.
174+
fn snapshot_index(
175+
snapshot_tree: &mut gix::object::tree::Editor,
176+
base_tree: gix::Tree,
177+
changes: but_core::WorktreeChanges,
178+
selection: BTreeSet<BString>,
179+
) -> anyhow::Result<Option<(Option<gix::ObjectId>, Option<gix::ObjectId>)>> {
180+
let mut conflicts = Vec::new();
181+
let changes: Vec<_> = changes
182+
.changes
183+
.into_iter()
184+
.filter_map(|c| c.status_item)
185+
.chain(
186+
changes
187+
.ignored_changes
188+
.into_iter()
189+
.filter_map(|c| c.status_item),
190+
)
191+
.filter_map(|item| match item {
192+
gix::status::Item::IndexWorktree(
193+
gix::status::index_worktree::Item::Modification {
194+
status: EntryStatus::Conflict { entries, .. },
195+
rela_path,
196+
..
197+
},
198+
) => {
199+
conflicts.push((rela_path, entries));
200+
None
201+
}
202+
gix::status::Item::TreeIndex(c) => Some(c),
203+
_ => None,
204+
})
205+
.filter(|c| selection.iter().any(|path| path == c.location()))
206+
.collect();
207+
208+
if changes.is_empty() && conflicts.is_empty() {
209+
return Ok(None);
210+
}
211+
212+
let mut base_tree_edit = base_tree.edit()?;
213+
for change in changes {
214+
match change {
215+
Change::Deletion { location, .. } => {
216+
base_tree_edit.remove(location.as_bstr())?;
217+
}
218+
Change::Addition {
219+
location,
220+
entry_mode,
221+
id,
222+
..
223+
}
224+
| Change::Modification {
225+
location,
226+
entry_mode,
227+
id,
228+
..
229+
} => {
230+
base_tree_edit.upsert(
231+
location.as_bstr(),
232+
entry_mode
233+
.to_tree_entry_mode()
234+
.with_context(|| format!("Could not convert the index entry {entry_mode:?} at '{location}' into a tree entry kind"))?
235+
.kind(),
236+
id.into_owned(),
237+
)?;
238+
}
239+
Change::Rewrite { .. } => {
240+
unreachable!("BUG: this must have been deactivated")
241+
}
242+
}
243+
}
244+
245+
let index = base_tree_edit.write()?;
246+
let index = (index != base_tree.id).then_some(index.detach());
247+
if let Some(index) = index {
248+
snapshot_tree.upsert("index", EntryKind::Tree, index)?;
249+
}
250+
251+
let index_conflicts = if conflicts.is_empty() {
252+
None
253+
} else {
254+
let mut root = snapshot_tree.cursor_at("index-conflicts")?;
255+
for stage in 1..=3 {
256+
for (rela_path, conflict_at_stage) in
257+
conflicts.iter_mut().filter_map(|(rela_path, stages)| {
258+
#[allow(clippy::indexing_slicing)]
259+
stages[stage - 1].take().map(|e| (rela_path, e))
260+
})
261+
{
262+
root.upsert(
263+
format!("{stage}/{rela_path}"),
264+
conflict_at_stage
265+
.mode
266+
.to_tree_entry_mode()
267+
.with_context(|| {
268+
format!(
269+
"Could not convert the index entry {entry_mode:?} \
270+
at '{location}' into a tree entry kind",
271+
entry_mode = conflict_at_stage.mode,
272+
location = rela_path
273+
)
274+
})?
275+
.kind(),
276+
conflict_at_stage.id,
277+
)?;
278+
}
279+
}
280+
Some(root.write()?.detach())
281+
};
282+
283+
Ok(Some((index, index_conflicts)))
284+
}
156285
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub struct Outcome<'repo> {
66
/// It's `None` if the was no worktree change.
77
pub worktree_cherry_pick: Option<gix::merge::tree::Outcome<'repo>>,
88
/// If an index was stored in the snapshot, this is the reconstructed index, including conflicts.
9+
/// Note that it has no information from disk whatsoever and should not be written like that.
910
///
1011
/// It's `None` if there were no index-only changes.
1112
pub index: Option<gix::index::State>,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
3+
### Description
4+
# A newly initialized git repository with an executable, a normal file, a symlink and a fifo, added to the index.
5+
set -eu -o pipefail
6+
7+
git init
8+
echo content > untracked
9+
echo exe > untracked-exe && chmod +x untracked-exe
10+
ln -s untracked link
11+
mkdir dir
12+
mkfifo dir/fifo-should-be-ignored
13+
14+
git add .
15+

0 commit comments

Comments
 (0)