Skip to content

Commit 692022c

Browse files
committed
Implement snapshot::create_tree() with worktree snapshot supports
1 parent 815a4bd commit 692022c

File tree

9 files changed

+343
-101
lines changed

9 files changed

+343
-101
lines changed

crates/but-core/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,9 @@ pub enum IgnoredWorktreeTreeChangeStatus {
332332
pub struct IgnoredWorktreeChange {
333333
/// The worktree-relative path to the change.
334334
#[serde(serialize_with = "gitbutler_serde::bstring_lossy::serialize")]
335-
path: BString,
335+
pub path: BString,
336336
/// The status that caused this change to be ignored.
337-
status: IgnoredWorktreeTreeChangeStatus,
337+
pub status: IgnoredWorktreeTreeChangeStatus,
338338
}
339339

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

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,28 @@ pub fn create_commit(
231231
bail!("cannot currently handle more than 1 parent")
232232
}
233233

234+
let target_tree = match &destination {
235+
Destination::NewCommit {
236+
parent_commit_id: None,
237+
..
238+
} => gix::ObjectId::empty_tree(repo.object_hash()),
239+
Destination::NewCommit {
240+
parent_commit_id: Some(base_commit),
241+
..
242+
}
243+
| Destination::AmendCommit {
244+
commit_id: base_commit,
245+
..
246+
} => but_core::Commit::from_id(base_commit.attach(repo))?
247+
.tree_id_or_auto_resolution()?
248+
.detach(),
249+
};
250+
234251
let CreateTreeOutcome {
235252
rejected_specs,
236253
destination_tree,
237254
changed_tree_pre_cherry_pick,
238-
} = create_tree(repo, &destination, move_source, changes, context_lines)?;
255+
} = create_tree(repo, target_tree, move_source, changes, context_lines)?;
239256
let new_commit = if let Some(new_tree) = destination_tree {
240257
match destination {
241258
Destination::NewCommit {

crates/but-workspace/src/commit_engine/tree/mod.rs

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::commit_engine::{Destination, MoveSourceCommit, RejectionReason, apply_hunks};
1+
use crate::commit_engine::{MoveSourceCommit, RejectionReason, apply_hunks};
22
use crate::{DiffSpec, HunkHeader};
33
use bstr::{BStr, ByteSlice};
44
use but_core::{RepositoryExt, UnifiedDiff};
@@ -17,8 +17,8 @@ pub struct CreateTreeOutcome {
1717
/// when merging the workspace commit, or because the specified hunks didn't match exactly due to changes
1818
/// that happened in the meantime, or if a file without a change was specified.
1919
pub rejected_specs: Vec<(RejectionReason, DiffSpec)>,
20-
/// The newly created seen from tree that acts as the destination of the changes, or `None` if no commit could be
21-
/// created as all changes-requests were rejected.
20+
/// The newly created seen from tree that acts as the destination of the changes, or `None` if no tree could be
21+
/// created as all changes-requests were rejected (or there was no change).
2222
pub destination_tree: Option<gix::ObjectId>,
2323
/// If `destination_tree` is `Some(_)`, this field is `Some(_)` as well and denotes the base-tree + all changes.
2424
/// If the applied changes were from the worktree, it's `HEAD^{tree}` + changes.
@@ -29,28 +29,11 @@ pub struct CreateTreeOutcome {
2929
/// Like [`create_commit()`], but lower-level and only returns a new tree, without finally associating it with a commit.
3030
pub fn create_tree(
3131
repo: &gix::Repository,
32-
destination: &Destination,
32+
target_tree: gix::ObjectId,
3333
move_source: Option<MoveSourceCommit>,
3434
changes: Vec<DiffSpec>,
3535
context_lines: u32,
3636
) -> anyhow::Result<CreateTreeOutcome> {
37-
let target_tree = match destination {
38-
Destination::NewCommit {
39-
parent_commit_id: None,
40-
..
41-
} => gix::ObjectId::empty_tree(repo.object_hash()),
42-
Destination::NewCommit {
43-
parent_commit_id: Some(base_commit),
44-
..
45-
}
46-
| Destination::AmendCommit {
47-
commit_id: base_commit,
48-
..
49-
} => but_core::Commit::from_id(base_commit.attach(repo))?
50-
.tree_id_or_auto_resolution()?
51-
.detach(),
52-
};
53-
5437
let mut changes: Vec<_> = changes.into_iter().map(Ok).collect();
5538
let (new_tree, changed_tree_pre_cherry_pick) = if changes.is_empty() {
5639
(Some(target_tree), None)
@@ -81,7 +64,7 @@ pub fn create_tree(
8164
let tree_with_changes = if new_tree == actual_base_tree
8265
&& changes.iter().all(|c| {
8366
c.is_ok()
84-
// Some rejections are OK and we want to create a commit anyway.
67+
// Some rejections are OK, and we want to create a commit anyway.
8568
|| !matches!(
8669
c,
8770
Err((RejectionReason::CherryPickMergeConflict,_))
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use bstr::BString;
2+
use but_graph::VirtualBranchesTomlMetadata;
3+
use std::collections::BTreeSet;
4+
5+
/// A way to determine what should be included in the snapshot when calling [create_tree()](function::create_tree).
6+
#[derive(Debug, Clone)]
7+
pub struct State {
8+
/// The result of a previous worktree changes call.
9+
///
10+
/// It contains detailed information about the complete set of possible changes to become part of the worktree.
11+
pub changes: but_core::WorktreeChanges,
12+
/// Repository-relative and slash-separated paths that match any change in the [`changes`](State::changes) field.
13+
/// It is *not* error if there is no match, as there can be snapshots without working tree changes, but with other changes.
14+
/// It's up to the caller to check for that via [`Outcome::is_empty()`].
15+
pub selection: BTreeSet<BString>,
16+
/// If `true`, store the current `HEAD` reference, i.e. its target, as well as the targets of all refs it's pointing to by symbolic link.
17+
pub head: bool,
18+
}
19+
20+
/// Contains all state that the snapshot contains.
21+
#[derive(Debug, Copy, Clone)]
22+
pub struct Outcome {
23+
/// The snapshot itself, with all the subtrees available that are also listed in this structure.
24+
pub snapshot_tree: gix::ObjectId,
25+
/// For good measure, the input `HEAD^{tree}` that is used as the basis to learn about worktree changes.
26+
pub head_tree: gix::ObjectId,
27+
/// The `head_tree` with the selected worktree changes applied, suitable for being stored in a commit,
28+
/// or `None` if there was no change in the worktree.
29+
pub worktree: Option<gix::ObjectId>,
30+
/// The tree representing the current changed index, without conflicts, or `None` if there was no change to the index.
31+
pub index: Option<gix::ObjectId>,
32+
/// A tree with files in a custom storage format to allow keeping conflicting blobs reachable, along with detailed conflict information
33+
/// to allow restoring the conflict entries in the index.
34+
pub index_conflicts: Option<gix::ObjectId>,
35+
/// The tree representing the reference targets of all references within the *workspace*.
36+
pub workspace_references: Option<gix::ObjectId>,
37+
/// The tree representing the reference targets of all references reachable from `HEAD`, so typically `HEAD` itself, and the
38+
/// target object of the reference it is pointing to.
39+
pub head_references: Option<gix::ObjectId>,
40+
/// The tree representing the metadata of all references within the *workspace*.
41+
pub metadata: Option<gix::ObjectId>,
42+
}
43+
44+
impl Outcome {
45+
/// Return `true` if the snapshot contains no information whatsoever, which is equivalent to being an empty tree.
46+
pub fn is_empty(&self) -> bool {
47+
self.snapshot_tree.is_empty_tree()
48+
}
49+
}
50+
51+
/// A utility to more easily use *no* workspace or metadata.
52+
pub fn no_workspace_and_meta() -> Option<(
53+
&'static but_graph::projection::Workspace<'static>,
54+
&'static VirtualBranchesTomlMetadata,
55+
)> {
56+
None
57+
}
58+
59+
pub(super) mod function {
60+
use super::{Outcome, State};
61+
use crate::{DiffSpec, commit_engine};
62+
use anyhow::bail;
63+
use but_core::RefMetadata;
64+
use gix::object::tree::EntryKind;
65+
use tracing::instrument;
66+
67+
/// Create a tree that represents the snapshot for the given `selection`, with the basis for everything
68+
/// being the `head_tree_id` *(i.e. the tree to which `HEAD` is ultimately pointing to)*.
69+
/// Make this an empty tree if the `HEAD` is unborn.
70+
///
71+
/// If `workspace_and_meta` is not `None`, the workspace and metadata to store in the snapshot.
72+
/// We will only store reference positions, and assume that their commits are safely stored in the reflog to not
73+
/// be garbage collected. Metadata is only stored for the references that are included in the `workspace`.
74+
///
75+
/// Note that objects will be written into the repository behind `head_tree_id` unless it's configured
76+
/// to keep everything in memory.
77+
///
78+
/// ### Snapshot Tree Format
79+
///
80+
/// There are the following top-level trees, with their own sub-formats which aren't specified here.
81+
/// However, it's notable that they have to be implemented so that they remain compatible to prior versions
82+
/// of the tree.
83+
///
84+
/// Note that all top-level entries are optional, and only present if there is a snapshot to store.
85+
///
86+
/// * `HEAD`
87+
/// - the tree to which `HEAD` was pointing at the time the snapshot was created.
88+
/// - this is relevant when re-applying the worktree-changes and when recreating the `index`.
89+
/// - only set if it is needed to restore some of the snapshot state.
90+
/// * `worktree`
91+
/// - the tree of `HEAD + uncommitted files`. Technically this means that now possibly untracked files are known to Git,
92+
/// even though it might be that the respective objects aren't written to disk yet.
93+
/// - Note that this tree may contain files with conflict markers as it will pick up the conflicting state visible on disk.
94+
/// * `index`
95+
/// - A representation of the non-conflicting portions of the index, without its meta-data.
96+
#[instrument(skip(changes, _workspace_and_meta), err(Debug))]
97+
pub fn create_tree(
98+
head_tree_id: gix::Id<'_>,
99+
State {
100+
changes,
101+
selection,
102+
head: _,
103+
}: State,
104+
_workspace_and_meta: Option<(&but_graph::projection::Workspace, &impl RefMetadata)>,
105+
) -> anyhow::Result<Outcome> {
106+
let repo = head_tree_id.repo;
107+
// TODO: refactor tree-creation to not assume commits anymore.
108+
let mut changes_to_apply: Vec<_> = changes
109+
.changes
110+
.iter()
111+
.filter(|c| selection.contains(&c.path))
112+
.map(|c| Ok(DiffSpec::from(c)))
113+
.collect();
114+
let (new_tree, base_tree) = commit_engine::tree::apply_worktree_changes(
115+
head_tree_id.into(),
116+
repo,
117+
&mut changes_to_apply,
118+
0, /* context lines don't matter */
119+
)?;
120+
121+
let rejected = changes_to_apply
122+
.into_iter()
123+
.filter_map(Result::err)
124+
.collect::<Vec<_>>();
125+
if !rejected.is_empty() {
126+
bail!(
127+
"It should be impossible to fail to apply changes that are in the tree that was provided as HEAD^{{tree}} - {rejected:?}"
128+
)
129+
}
130+
131+
let mut edit = repo.empty_tree().edit()?;
132+
133+
let worktree = (new_tree != base_tree).then_some(new_tree.detach());
134+
let mut needs_head = false;
135+
if let Some(worktree) = worktree {
136+
edit.upsert("worktree", EntryKind::Tree, worktree)?;
137+
needs_head = true;
138+
}
139+
140+
if needs_head {
141+
edit.upsert("HEAD", EntryKind::Tree, head_tree_id)?;
142+
}
143+
144+
Ok(Outcome {
145+
snapshot_tree: edit.write()?.into(),
146+
head_tree: head_tree_id.into(),
147+
worktree,
148+
index: None,
149+
index_conflicts: None,
150+
workspace_references: None,
151+
head_references: None,
152+
metadata: None,
153+
})
154+
}
155+
}

crates/but-workspace/src/snapshot.rs renamed to crates/but-workspace/src/snapshot/mod.rs

Lines changed: 27 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,7 @@
11
//! The ability to create a Git representation of diverse 'state' that can be restored at a later time.
22
3-
///
4-
pub mod create_tree {
5-
use bstr::BString;
6-
7-
/// A way to determine what should be included in the snapshot when calling [create_tree()](function::create_tree).
8-
pub struct State<'a> {
9-
/// The result of a previous worktree changes call.
10-
///
11-
/// It contains detailed information about the complete set of possible changes to become part of the worktree.
12-
pub changes: &'a but_core::WorktreeChanges,
13-
/// Repository-relative and slash-separated paths that match any change in the [`changes`](State::changes) field.
14-
/// **It's an error if there is no match.** as there is not supposed to be a snapshot without a change to the working tree.
15-
pub selection: Vec<BString>,
16-
/// If `true`, store the current `HEAD` reference, i.e. its target, as well as the targets of all refs it's pointing to by symbolic link.
17-
pub head: bool,
18-
}
19-
20-
/// Contains all state that the snapshot contains.
21-
#[derive(Debug, Copy, Clone)]
22-
pub struct Outcome {
23-
/// The snapshot itself, with all the subtrees available that are also listed in this structure.
24-
pub snapshot_tree: gix::ObjectId,
25-
/// For good measure, the input `HEAD^{tree}` that is used as the basis to learn about worktree changes.
26-
pub head_tree: gix::ObjectId,
27-
/// The `head_tree` with the selected worktree changes applied, suitable for being stored in a commit.
28-
pub wortree: gix::ObjectId,
29-
/// The tree representing the current changed index, without conflicts, or `None` if there was no change to the index.
30-
pub index: Option<gix::ObjectId>,
31-
/// A tree with files in a custom storage format to allow keeping conflicting blobs reachable, along with detailed conflict information
32-
/// to allow restoring the conflict entries in the index.
33-
pub index_conflicts: Option<gix::ObjectId>,
34-
/// The tree representing the reference targets of all references within the *workspace*.
35-
pub workspace_references: Option<gix::ObjectId>,
36-
/// The tree representing the reference targets of all references reachable from `HEAD`, so typically `HEAD` itself, and the
37-
/// target object of the reference it is pointing to.
38-
pub head_references: Option<gix::ObjectId>,
39-
/// The tree representing the metadata of all references within the *workspace*.
40-
pub metadata: Option<gix::ObjectId>,
41-
}
42-
43-
pub(super) mod function {
44-
use super::{Outcome, State};
45-
use but_core::RefMetadata;
46-
/// Create a tree that represents the snapshot for the given `selection`, with the basis for everything
47-
/// being the `head_tree_id` (i.e. the tree to which `HEAD` is ultimately pointing to).
48-
///
49-
/// If `workspace_and_meta` is not `None`, the workspace and metadata to store in the snapshot.
50-
/// We will only store reference positions, and assume that their commits are safely stored in the reflog to not
51-
/// be garbage collected. Metadata is only stored for the references that are included in the `workspace`.
52-
///
53-
/// Note that objects will be written into the repository behind `head_tree_id` unless it's configured
54-
/// to keep everything in memory.
55-
pub fn create_tree(
56-
_head_tree_id: gix::Id<'_>,
57-
_selection: State,
58-
_workspace_and_meta: Option<(&but_graph::projection::Workspace, &impl RefMetadata)>,
59-
) -> anyhow::Result<Outcome> {
60-
todo!()
61-
}
62-
}
63-
}
3+
/// Structures to call the [create_tree()] function.
4+
pub mod create_tree;
645
pub use create_tree::function::create_tree;
656

667
/// Utilities related to resolving previously created snapshots.
@@ -70,9 +11,12 @@ pub mod resolve_tree {
7011
pub struct Outcome<'repo> {
7112
/// The cherry-pick result as merge between the target worktree and the snapshot, **possibly with conflicts**.
7213
///
73-
/// This tree, may be checked out to the working tree, with or without conflicts.
74-
pub workspace_cherry_pick: gix::merge::tree::Outcome<'repo>,
14+
/// This tree, may be checked out to the working tree, with or without conflicts - it's entirely left to the caller.
15+
/// It's `None` if the was no worktree change.
16+
pub workspace_cherry_pick: Option<gix::merge::tree::Outcome<'repo>>,
7517
/// If an index was stored in the snapshot, this is the reconstructed index, including conflicts.
18+
///
19+
/// It's `None` if there were no index-only changes.
7620
pub index: Option<gix::index::State>,
7721
/// Reference edits that when applied in a transaction will set the workspace back to where it was. Only available
7822
/// if it was part of the snapshot to begin with.
@@ -89,22 +33,33 @@ pub mod resolve_tree {
8933
pub branches: Vec<(gix::refs::FullName, but_core::ref_metadata::Branch)>,
9034
}
9135

92-
pub(super) mod function {
93-
use super::Outcome;
36+
/// Options for use in [super::resolve_tree()].
37+
#[derive(Debug, Clone)]
38+
pub struct Options {
39+
/// If set, the non-default options to use when cherry-picking the worktree changes onto the target tree.
40+
///
41+
/// If `None`, perform the merge just like Git.
42+
pub workspace_cherry_pick: Option<gix::merge::tree::Options>,
43+
}
9444

45+
pub(super) mod function {
46+
use super::{Options, Outcome};
9547
/// Given the `snapshot_tree` as previously returned via [super::create_tree::Outcome::snapshot_tree], extract data and…
9648
///
9749
/// * …cherry-pick the worktree changes onto the `target_worktree_tree_id`, which is assumed to represent the future working directory state
98-
/// and which either contains the worktree changes or *preferably* is the `HEAD^{tree}` as the working directory is clean.
50+
/// and which either contains the worktree changes or *preferably* is the `HEAD^{tree}` as the working directory is clean.
9951
/// * …reconstruct the index to write into `.git/index`, assuming that the current `.git/index` is clean.
10052
/// * …produce reference edits to put the workspace refs back into place with.
10153
/// * …produce metadata that if set will represent the metadata of the entire workspace.
10254
///
10355
/// Note that none of this data is actually manifested in the repository or working tree, they only exists as objects in the Git database,
10456
/// assuming in-memory objects aren't used in the repository.
105-
pub fn resolve_tree<'repo>(
57+
pub fn resolve_tree(
10658
_snapshot_tree: gix::Id<'_>,
10759
_target_worktree_tree_id: gix::ObjectId,
60+
Options {
61+
workspace_cherry_pick: _,
62+
}: Options,
10863
) -> anyhow::Result<Outcome<'_>> {
10964
todo!()
11065
}
@@ -152,13 +107,13 @@ mod commit {
152107
type Err = anyhow::Error;
153108

154109
fn from_str(s: &str) -> anyhow::Result<Self, Self::Err> {
155-
let parts: Vec<&str> = s.splitn(2, ':').collect();
156-
if parts.len() != 2 {
110+
let mut parts = s.splitn(2, ':');
111+
let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
157112
return Err(anyhow!("Invalid trailer format, expected `key: value`"));
158-
}
159-
let unescaped_value = parts[1].trim().replace("\\n", "\n");
113+
};
114+
let unescaped_value = value.trim().replace("\\n", "\n");
160115
Ok(Self {
161-
key: parts[0].trim().to_string(),
116+
key: key.trim().to_string(),
162117
value: unescaped_value,
163118
})
164119
}

0 commit comments

Comments
 (0)