Skip to content

Commit f44ea3c

Browse files
committed
Sketch of the APIs involved in branch::apply().
1 parent c1e23e9 commit f44ea3c

File tree

2 files changed

+220
-1
lines changed

2 files changed

+220
-1
lines changed

crates/but-workspace/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![deny(missing_docs, rust_2018_idioms)]
1+
#![deny(missing_docs)]
22
#![deny(clippy::indexing_slicing)]
33

44
//! ### Terminology
@@ -55,6 +55,8 @@ pub use head::{head, merge_worktree_with_workspace};
5555
/// Ignore the name of this module; it's just a place to put code by now.
5656
pub mod branch;
5757

58+
pub mod snapshot;
59+
5860
mod changeset;
5961

6062
mod commit;

crates/but-workspace/src/snapshot.rs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//! The ability to create a Git representation of diverse 'state' that can be restored at a later time.
2+
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+
}
64+
pub use create_tree::function::create_tree;
65+
66+
/// Utilities related to resolving previously created snapshots.
67+
pub mod resolve_tree {
68+
69+
/// The information extracted from [`resolve_tree`](function::resolve_tree()).
70+
pub struct Outcome<'repo> {
71+
/// The cherry-pick result as merge between the target worktree and the snapshot, **possibly with conflicts**.
72+
///
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>,
75+
/// If an index was stored in the snapshot, this is the reconstructed index, including conflicts.
76+
pub index: Option<gix::index::State>,
77+
/// Reference edits that when applied in a transaction will set the workspace back to where it was. Only available
78+
/// if it was part of the snapshot to begin with.
79+
pub workspace_references: Option<Vec<gix::refs::transaction::RefEdit>>,
80+
/// The metadata to be applied to the ref-metadata store.
81+
pub metadata: Option<MetadataEdits>,
82+
}
83+
84+
/// Edits for application via [`but_core::RefMetadata`].
85+
pub struct MetadataEdits {
86+
/// The workspace metadata stored in the snapshot.
87+
pub workspace: (gix::refs::FullName, but_core::ref_metadata::Workspace),
88+
/// The branch metadata stored in snapshots.
89+
pub branches: Vec<(gix::refs::FullName, but_core::ref_metadata::Branch)>,
90+
}
91+
92+
pub(super) mod function {
93+
use super::Outcome;
94+
95+
/// Given the `snapshot_tree` as previously returned via [super::create_tree::Outcome::snapshot_tree], extract data and…
96+
///
97+
/// * …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.
99+
/// * …reconstruct the index to write into `.git/index`, assuming that the current `.git/index` is clean.
100+
/// * …produce reference edits to put the workspace refs back into place with.
101+
/// * …produce metadata that if set will represent the metadata of the entire workspace.
102+
///
103+
/// Note that none of this data is actually manifested in the repository or working tree, they only exists as objects in the Git database,
104+
/// assuming in-memory objects aren't used in the repository.
105+
pub fn resolve_tree<'repo>(
106+
_snapshot_tree: gix::Id<'_>,
107+
_target_worktree_tree_id: gix::ObjectId,
108+
) -> anyhow::Result<Outcome<'_>> {
109+
todo!()
110+
}
111+
}
112+
}
113+
pub use resolve_tree::function::resolve_tree;
114+
115+
/// Utilities for associating snapshot-trees with commits and additional metadata.
116+
mod commit {
117+
use anyhow::anyhow;
118+
use but_core::RefMetadata;
119+
use serde::Serialize;
120+
use std::fmt;
121+
use std::fmt::{Display, Formatter};
122+
use std::str::FromStr;
123+
124+
/// A commit representing a snapshot, along with metadata.
125+
#[expect(dead_code)]
126+
pub struct Commit<'repo> {
127+
/// The id of the commit that was used for accessing its metadata.
128+
id: gix::Id<'repo>,
129+
/// The fully decoded commit.
130+
inner: gix::objs::Commit,
131+
}
132+
133+
/// Represents a key value pair stored in a snapshot, like `key: value\n`
134+
/// Using the git trailer format (<https://git-scm.com/docs/git-interpret-trailers>)
135+
#[derive(Debug, PartialEq, Clone, Serialize)]
136+
#[serde(rename_all = "camelCase")]
137+
pub struct CommitTrailer {
138+
/// Trailer key.
139+
pub key: String,
140+
/// Trailer value.
141+
pub value: String,
142+
}
143+
144+
impl Display for CommitTrailer {
145+
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
146+
let escaped_value = self.value.replace('\n', "\\n");
147+
write!(f, "{}: {}", self.key, escaped_value)
148+
}
149+
}
150+
151+
impl FromStr for CommitTrailer {
152+
type Err = anyhow::Error;
153+
154+
fn from_str(s: &str) -> anyhow::Result<Self, Self::Err> {
155+
let parts: Vec<&str> = s.splitn(2, ':').collect();
156+
if parts.len() != 2 {
157+
return Err(anyhow!("Invalid trailer format, expected `key: value`"));
158+
}
159+
let unescaped_value = parts[1].trim().replace("\\n", "\n");
160+
Ok(Self {
161+
key: parts[0].trim().to_string(),
162+
value: unescaped_value,
163+
})
164+
}
165+
}
166+
167+
/// Metadata attached to [`Commit`]s holding snapshots.
168+
pub struct CommitMetadata {
169+
/// The name of the operation that created the commit.
170+
/// This is an internal string.
171+
pub operation: String,
172+
/// The title of the commit for user consumption, typically created using information from `trailers`.
173+
pub title: String,
174+
/// Properties to be stored with the commit.
175+
pub trailers: Vec<CommitTrailer>,
176+
}
177+
178+
/// Given a `snapshot_tree` as created by [`super::create_tree()`], associate it with the stash of `ref_name`.
179+
/// If a stash already exists, put it on top, with a new commit to carry `metadata`.
180+
pub fn create_stash_commit<'repo>(
181+
_snapshot_tree: gix::Id<'repo>,
182+
_ref_name: &gix::refs::FullNameRef,
183+
_metadata: CommitMetadata,
184+
) -> anyhow::Result<Commit<'repo>> {
185+
todo!()
186+
}
187+
188+
/// List all stash commits available for `ref_name`, with the top-most (most recent) first, and the oldest one last.
189+
pub fn list_stash_commits<'repo>(
190+
_repo: &'repo gix::Repository,
191+
_ref_name: &gix::refs::FullNameRef,
192+
) -> anyhow::Result<Vec<Commit<'repo>>> {
193+
todo!()
194+
}
195+
196+
/// List all references for which a stash is available.
197+
/// Note that these might not actually exist in the `repo`, for instance if the actual reference was renamed.
198+
pub fn list_stash_references(_repo: &gix::Repository) -> Vec<gix::refs::FullName> {
199+
todo!()
200+
}
201+
202+
/// Remove the top-most stash from the top of `ref_name` and write back all changes.
203+
/// Just like Git, write merge conflicts and update the index, possibly update refs and metadata.
204+
// TODO: should there be a dry-run version of this to know if it will conflict or not? Who likes having merge-conflicts
205+
// in the worktree and no stash afterwards? Needs separate method to avoid touching refs.
206+
pub fn pop_stash_commit(
207+
_repo: &gix::Repository,
208+
_ref_name: &gix::refs::FullNameRef,
209+
_meta: &mut impl RefMetadata,
210+
) -> anyhow::Result<()> {
211+
todo!()
212+
}
213+
}
214+
pub use commit::{
215+
Commit, CommitMetadata, CommitTrailer, create_stash_commit, list_stash_commits,
216+
list_stash_references, pop_stash_commit,
217+
};

0 commit comments

Comments
 (0)