Skip to content

Commit 4abed5d

Browse files
committed
Create a minimal version of but-oplog that is mostly legacy, to enable but-api
That way we can demonstrate semantics of the oplog/undo system.
1 parent 4079065 commit 4abed5d

File tree

8 files changed

+142
-8
lines changed

8 files changed

+142
-8
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ but-settings = { path = "crates/but-settings" }
3737
but-oxidize = { path = "crates/but-oxidize" }
3838
but-fs = { path = "crates/but-fs" }
3939
but-forge = { path = "crates/but-forge" }
40+
but-oplog = { path = "crates/but-oplog" }
4041

4142
gitbutler-git = { path = "crates/gitbutler-git" }
4243
gitbutler-watcher = { path = "crates/gitbutler-watcher" }

crates/but-oplog/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "but-oplog"
3+
version = "0.0.0"
4+
edition = "2024"
5+
authors = ["GitButler <gitbutler@gitbutler.com>"]
6+
publish = false
7+
rust-version = "1.89"
8+
description = "An implementation of an undo queue implemented via restore-points"
9+
10+
[lib]
11+
doctest = false
12+
13+
[features]
14+
legacy = ["dep:gitbutler-oplog"]
15+
16+
[dependencies]
17+
but-ctx.workspace = true
18+
but-oxidize.workspace = true
19+
20+
gitbutler-oplog = { workspace = true, optional = true }
21+
22+
gix.workspace = true
23+
anyhow = "1.0.100"

crates/but-oplog/src/lib.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//! The operations log, short: Oplog, is a sequence of restore points, which each restore point implemented as snapshots.
2+
//! Snapshots contain enough information to restore what we are interested in.
3+
//!
4+
//! ### Snapshots
5+
//!
6+
//! For simplicity, snapshots of "what we are interested in" is all state that contributes to the GitButler experience, such as:
7+
//!
8+
//! * where `HEAD` points to
9+
//! * the position of all references in the Workspace
10+
//! - the positions of stash references
11+
//! * metadata stored in the database
12+
//!
13+
//! Note the absence of uncommitted and untracked files, as it's something we'd not want to track for the user - they have to do this
14+
//! themselves and voluntarily via stashes. We only manipulate data that is stored in Git or that is owned and maintained by GitButler.
15+
//!
16+
//! It's controlled by the caller which of these (or parts) are stored and to what extent just as an optimisation.
17+
//!
18+
//! ### The Log
19+
//!
20+
//! Snapshots are merely Git trees that contain information, and they can be arranged using commits to bring them in order, and to
21+
//! attach metadata about the operation that created them.
22+
//!
23+
//! This is where the Log part comes into play, as it associates commits with these trees, and the parent-child relationships of these commits
24+
//! form the log.
25+
//!
26+
//! #### Undo/Redo
27+
//!
28+
//! Undo can be implemented by keeping track of which position in the Log we are currently in using its Head, and by restoring all relevant state
29+
//! to the snapshot it's pointing to. To support a redo, and if this is the most recent entry in the Log as pointed to by its tip, we'd have to
30+
//! create another snapshot to capture that state so there is something to go back to.
31+
//!
32+
//! From that point on, one can move freely through the log backwards and forwards. Worktree changes complicate this, because it's unclear to me to what
33+
//! extent we should even handle them - my take is to not keep them in oplog snapshots at all and force the user to stash them away.
34+
//!
35+
//! When creating a new snapshot while the head isn't at the tip of the log, for simplicity we "forget" the snapshots between the head and the tip, and
36+
//! move the tip to the new snapshot instead.
37+
//!
38+
//! ### Mode of operation: Side Effect
39+
//!
40+
//! When applying a mutation the repository, the oplog runs as side . The *effect* itself changes state, and is expected to either fully succeed,
41+
//! or leave no trace of ever running.
42+
//!
43+
//! This means that snapshots have to be recorded in such a way that they can't be observed until they are committed, which happens only when the
44+
//! *effect* succeeds. On failure, there is no entry in the oplog.
45+
//!
46+
//! ### Status of the implementation
47+
//!
48+
//! Right now it's merely a letter of intend with an API sketch that is sufficient to 'record but not persist unless command is successful'.
49+
//! Note that the `gitbutler-oplog` is still the backbone of this implementation, but modified to the extent necessary.
50+
//! Legacy commands will always see their restore point created as their failure might leave partial changes that might still be undoable
51+
//! with the restore point.
52+
//!
53+
//! Non-legacy commands can use the [`UnmaterializedOplogSnapshot`] utility to insert the snapshot into the log on successful effects.
54+
55+
/// This is just a sketch for an in-memory snapshot that isn't observable through the on-disk repository.
56+
/// It will be committed only if the main effect of a function was successfully applied.
57+
/// This works only if that effect is known to only apply in full, or not at all (at least in 99.9% of the cases).
58+
///
59+
/// NOTE: if this utility type should really take a `Context` as parameter, it should be in `but-api`.
60+
pub struct UnmaterializedOplogSnapshot {
61+
/// The tree containing all snapshot information.
62+
#[cfg(feature = "legacy")]
63+
tree_id: gix::ObjectId,
64+
#[cfg(feature = "legacy")]
65+
details: gitbutler_oplog::entry::SnapshotDetails,
66+
}
67+
68+
#[cfg(feature = "legacy")]
69+
mod oplog_snapshot {
70+
use crate::UnmaterializedOplogSnapshot;
71+
use but_oxidize::{ObjectIdExt, OidExt};
72+
use gitbutler_oplog::OplogExt;
73+
74+
/// Lifecycle
75+
impl UnmaterializedOplogSnapshot {
76+
/// Create a new instance from `details`, which is a snapshot that isn't committed to the oplog yet.
77+
/// This fails if the snapshot creation fails.
78+
pub fn from_details(
79+
ctx: &but_ctx::Context,
80+
details: gitbutler_oplog::entry::SnapshotDetails,
81+
) -> anyhow::Result<Self> {
82+
// TODO: these guards are probably something to remove as they don't belong into a plumbing crate, neither does Context.
83+
let guard = ctx.shared_worktree_access();
84+
let tree_id = ctx.prepare_snapshot(guard.read_permission())?.to_gix();
85+
Ok(Self { tree_id, details })
86+
}
87+
}
88+
89+
impl UnmaterializedOplogSnapshot {
90+
/// Call this method only if the main effect succeeded so the snapshot should be added to the operation log.
91+
pub fn commit(self, ctx: &but_ctx::Context) -> anyhow::Result<()> {
92+
let mut guard = ctx.exclusive_worktree_access();
93+
let _commit_id = ctx.commit_snapshot(
94+
self.tree_id.to_git2(),
95+
self.details,
96+
guard.write_permission(),
97+
)?;
98+
Ok(())
99+
}
100+
}
101+
}

crates/gitbutler-oplog/Cargo.toml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ edition = "2024"
55
authors = ["GitButler <gitbutler@gitbutler.com>"]
66
publish = false
77
rust-version = "1.89"
8-
autotests = false
98

109
[lib]
1110
doctest = false
@@ -33,10 +32,6 @@ tracing.workspace = true
3332
gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] }
3433
toml.workspace = true
3534

36-
[[test]]
37-
name = "oplog"
38-
path = "tests/mod.rs"
39-
4035
[dev-dependencies]
4136
pretty_assertions = "1.4"
4237
tempfile.workspace = true

crates/gitbutler-oplog/src/oplog.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,10 @@ fn get_workdir_tree(
372372
}
373373
}
374374

375-
fn prepare_snapshot(ctx: &Context, _shared_access: &WorktreeReadPermission) -> Result<git2::Oid> {
375+
pub fn prepare_snapshot(
376+
ctx: &Context,
377+
_shared_access: &WorktreeReadPermission,
378+
) -> Result<git2::Oid> {
376379
let repo = ctx.legacy_project.open_git2()?;
377380

378381
let vb_state = VirtualBranchesHandle::new(ctx.project_data_dir());

crates/gitbutler-oplog/src/snapshot.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::vec;
22

33
use anyhow::Result;
4-
use but_ctx::{Context, access::WorktreeWritePermission};
4+
use but_ctx::access::WorktreeWritePermission;
55
use gitbutler_branch::BranchUpdateRequest;
66
use gitbutler_reference::ReferenceName;
77
use gitbutler_stack::Stack;
@@ -79,7 +79,7 @@ pub trait SnapshotExt {
7979
}
8080

8181
/// Snapshot functionality
82-
impl SnapshotExt for Context {
82+
impl SnapshotExt for but_ctx::Context {
8383
fn snapshot_branch_unapplied(
8484
&self,
8585
snapshot_tree: git2::Oid,
File renamed without changes.

0 commit comments

Comments
 (0)