Skip to main content

reddb_server/application/
vcs.rs

1//! Version-control ("Git for Data") use cases.
2//!
3//! Sits on top of the MVCC snapshot manager and persists commit
4//! metadata in the `red_*` collections declared in
5//! [`crate::application::vcs_collections`]. Mirrors the git command
6//! surface: commit, branch, checkout, merge, cherry-pick, revert,
7//! reset, log, diff, status, tag.
8//!
9//! Designed to mirror the Graph/Query use-case pattern: thin struct
10//! parameterised over a [`RuntimeVcsPort`] trait implemented by
11//! `RedDBRuntime`. No storage logic lives here — only validation and
12//! delegation.
13
14use crate::application::ports::RuntimeVcsPort;
15use crate::json::Value as JsonValue;
16use crate::storage::transaction::snapshot::Xid;
17use crate::RedDBResult;
18
19// ---------------------------------------------------------------------------
20// Domain types (shared between application and runtime)
21// ---------------------------------------------------------------------------
22
23/// A commit hash. 64-char lowercase hex (SHA-256 truncated or full).
24pub type CommitHash = String;
25
26/// A full ref name like `refs/heads/main` or `refs/tags/v1.0`.
27pub type RefName = String;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Author {
31    pub name: String,
32    pub email: String,
33}
34
35#[derive(Debug, Clone)]
36pub struct Commit {
37    pub hash: CommitHash,
38    pub root_xid: Xid,
39    pub parents: Vec<CommitHash>,
40    pub height: u64,
41    pub author: Author,
42    pub committer: Author,
43    pub message: String,
44    /// Unix epoch milliseconds.
45    pub timestamp_ms: i64,
46    pub signature: Option<String>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum RefKind {
51    Branch,
52    Tag,
53    Head,
54}
55
56#[derive(Debug, Clone)]
57pub struct Ref {
58    pub name: RefName,
59    pub kind: RefKind,
60    /// For `Branch`/`Tag`: the commit hash pointed to.
61    /// For `Head`: the ref name it targets (resolved recursively).
62    pub target: String,
63    pub protected: bool,
64}
65
66// ---------------------------------------------------------------------------
67// Inputs
68// ---------------------------------------------------------------------------
69
70#[derive(Debug, Clone)]
71pub struct CreateCommitInput {
72    /// Connection id whose working set is being committed.
73    pub connection_id: u64,
74    pub message: String,
75    pub author: Author,
76    /// Optional override for committer (falls back to author).
77    pub committer: Option<Author>,
78    /// When true, re-commit on top of HEAD's current parent instead of
79    /// adding a new commit (git --amend semantics).
80    pub amend: bool,
81    /// When true and working set is empty, succeed silently instead of
82    /// erroring. Mirrors `git commit --allow-empty`.
83    pub allow_empty: bool,
84}
85
86#[derive(Debug, Clone)]
87pub struct CreateBranchInput {
88    pub name: String,
89    /// Optional base: ref or commit hash. Defaults to current HEAD of
90    /// the caller's connection.
91    pub from: Option<String>,
92    pub connection_id: u64,
93}
94
95#[derive(Debug, Clone)]
96pub struct CreateTagInput {
97    pub name: String,
98    /// Ref or commit hash to tag.
99    pub target: String,
100    pub annotation: Option<String>,
101}
102
103#[derive(Debug, Clone)]
104pub enum CheckoutTarget {
105    /// Short branch name (`main`) or full ref (`refs/heads/main`).
106    Branch(String),
107    /// Commit hash — detached HEAD.
108    Commit(CommitHash),
109    /// Tag name or full ref.
110    Tag(String),
111}
112
113#[derive(Debug, Clone)]
114pub struct CheckoutInput {
115    pub connection_id: u64,
116    pub target: CheckoutTarget,
117    /// Force checkout even if working set has uncommitted changes.
118    pub force: bool,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum MergeStrategy {
123    /// Fast-forward if possible, else create merge commit.
124    Auto,
125    /// Always create merge commit.
126    NoFastForward,
127    /// Fail if not fast-forward.
128    FastForwardOnly,
129}
130
131#[derive(Debug, Clone)]
132pub struct MergeOpts {
133    pub strategy: MergeStrategy,
134    /// Custom merge commit message; auto-generated if None.
135    pub message: Option<String>,
136    /// Abort merge and restore working set on conflict, instead of
137    /// leaving shadow docs in `red_conflicts`.
138    pub abort_on_conflict: bool,
139}
140
141impl Default for MergeOpts {
142    fn default() -> Self {
143        Self {
144            strategy: MergeStrategy::Auto,
145            message: None,
146            abort_on_conflict: false,
147        }
148    }
149}
150
151#[derive(Debug, Clone)]
152pub struct MergeInput {
153    pub connection_id: u64,
154    /// Branch or commit being merged into the current HEAD.
155    pub from: String,
156    pub opts: MergeOpts,
157    pub author: Author,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum ResetMode {
162    /// Move HEAD only. Working set preserved.
163    Soft,
164    /// Move HEAD and reset staged to target. Working set preserved.
165    Mixed,
166    /// Move HEAD, reset staged, discard working set.
167    Hard,
168}
169
170#[derive(Debug, Clone)]
171pub struct ResetInput {
172    pub connection_id: u64,
173    pub target: String,
174    pub mode: ResetMode,
175}
176
177#[derive(Debug, Clone, Default)]
178pub struct LogRange {
179    /// Upper bound ref / commit hash. Defaults to HEAD.
180    pub to: Option<String>,
181    /// Lower bound (exclusive). `from..to` semantics.
182    pub from: Option<String>,
183    pub limit: Option<usize>,
184    pub skip: Option<usize>,
185    /// When true, exclude merge commits.
186    pub no_merges: bool,
187}
188
189#[derive(Debug, Clone)]
190pub struct LogInput {
191    pub connection_id: u64,
192    pub range: LogRange,
193}
194
195#[derive(Debug, Clone)]
196pub struct DiffInput {
197    /// Ref, commit hash, or AS OF spec.
198    pub from: String,
199    pub to: String,
200    /// When set, restrict diff to this collection.
201    pub collection: Option<String>,
202    /// Omit entity bodies (metadata-only diff).
203    pub summary_only: bool,
204}
205
206#[derive(Debug, Clone)]
207pub struct StatusInput {
208    pub connection_id: u64,
209}
210
211// ---------------------------------------------------------------------------
212// AS OF time-travel spec
213// ---------------------------------------------------------------------------
214
215#[derive(Debug, Clone)]
216pub enum AsOfSpec {
217    Commit(CommitHash),
218    Branch(String),
219    Tag(String),
220    /// Unix epoch milliseconds.
221    TimestampMs(i64),
222    /// Raw transaction id (power users / diagnostics).
223    Snapshot(Xid),
224}
225
226// ---------------------------------------------------------------------------
227// Outputs
228// ---------------------------------------------------------------------------
229
230/// A single entity-level change between two commits.
231#[derive(Debug, Clone)]
232pub struct DiffEntry {
233    pub collection: String,
234    pub entity_id: String,
235    pub change: DiffChange,
236}
237
238#[derive(Debug, Clone)]
239pub enum DiffChange {
240    Added { after: JsonValue },
241    Removed { before: JsonValue },
242    Modified { before: JsonValue, after: JsonValue },
243}
244
245#[derive(Debug, Clone, Default)]
246pub struct Diff {
247    pub from: CommitHash,
248    pub to: CommitHash,
249    pub entries: Vec<DiffEntry>,
250    pub added: usize,
251    pub removed: usize,
252    pub modified: usize,
253}
254
255/// Result of a merge operation. Non-empty `conflicts` means the merge
256/// is paused — user must resolve shadow docs in `red_conflicts` before
257/// committing.
258#[derive(Debug, Clone)]
259pub struct MergeOutcome {
260    pub merge_commit: Option<Commit>,
261    pub fast_forward: bool,
262    pub conflicts: Vec<Conflict>,
263    pub merge_state_id: Option<String>,
264}
265
266impl MergeOutcome {
267    pub fn is_clean(&self) -> bool {
268        self.conflicts.is_empty()
269    }
270}
271
272#[derive(Debug, Clone)]
273pub struct Conflict {
274    pub id: String,
275    pub collection: String,
276    pub entity_id: String,
277    pub base: JsonValue,
278    pub ours: JsonValue,
279    pub theirs: JsonValue,
280    pub conflicting_paths: Vec<String>,
281    pub merge_state_id: String,
282}
283
284#[derive(Debug, Clone)]
285pub struct Status {
286    pub connection_id: u64,
287    pub head_ref: Option<RefName>,
288    pub head_commit: Option<CommitHash>,
289    pub detached: bool,
290    pub staged_changes: usize,
291    pub working_changes: usize,
292    pub unresolved_conflicts: usize,
293    pub merge_state_id: Option<String>,
294}
295
296// ---------------------------------------------------------------------------
297// Use-case surface
298// ---------------------------------------------------------------------------
299
300pub struct VcsUseCases<'a, P: ?Sized> {
301    runtime: &'a P,
302}
303
304impl<'a, P: RuntimeVcsPort + ?Sized> VcsUseCases<'a, P> {
305    pub fn new(runtime: &'a P) -> Self {
306        Self { runtime }
307    }
308
309    pub fn commit(&self, input: CreateCommitInput) -> RedDBResult<Commit> {
310        self.runtime.vcs_commit(input)
311    }
312
313    pub fn branch_create(&self, input: CreateBranchInput) -> RedDBResult<Ref> {
314        self.runtime.vcs_branch_create(input)
315    }
316
317    pub fn branch_list(&self) -> RedDBResult<Vec<Ref>> {
318        self.runtime.vcs_list_refs(Some("refs/heads/"))
319    }
320
321    pub fn branch_delete(&self, name: &str) -> RedDBResult<()> {
322        self.runtime.vcs_branch_delete(name)
323    }
324
325    pub fn tag(&self, input: CreateTagInput) -> RedDBResult<Ref> {
326        self.runtime.vcs_tag_create(input)
327    }
328
329    pub fn tag_list(&self) -> RedDBResult<Vec<Ref>> {
330        self.runtime.vcs_list_refs(Some("refs/tags/"))
331    }
332
333    pub fn checkout(&self, input: CheckoutInput) -> RedDBResult<Ref> {
334        self.runtime.vcs_checkout(input)
335    }
336
337    pub fn merge(&self, input: MergeInput) -> RedDBResult<MergeOutcome> {
338        self.runtime.vcs_merge(input)
339    }
340
341    pub fn cherry_pick(
342        &self,
343        connection_id: u64,
344        commit: &str,
345        author: Author,
346    ) -> RedDBResult<MergeOutcome> {
347        self.runtime.vcs_cherry_pick(connection_id, commit, author)
348    }
349
350    pub fn revert(&self, connection_id: u64, commit: &str, author: Author) -> RedDBResult<Commit> {
351        self.runtime.vcs_revert(connection_id, commit, author)
352    }
353
354    pub fn reset(&self, input: ResetInput) -> RedDBResult<()> {
355        self.runtime.vcs_reset(input)
356    }
357
358    pub fn log(&self, input: LogInput) -> RedDBResult<Vec<Commit>> {
359        self.runtime.vcs_log(input)
360    }
361
362    pub fn diff(&self, input: DiffInput) -> RedDBResult<Diff> {
363        self.runtime.vcs_diff(input)
364    }
365
366    pub fn status(&self, input: StatusInput) -> RedDBResult<Status> {
367        self.runtime.vcs_status(input)
368    }
369
370    pub fn lca(&self, a: &str, b: &str) -> RedDBResult<Option<CommitHash>> {
371        self.runtime.vcs_lca(a, b)
372    }
373
374    pub fn conflicts_list(&self, merge_state_id: &str) -> RedDBResult<Vec<Conflict>> {
375        self.runtime.vcs_conflicts_list(merge_state_id)
376    }
377
378    pub fn conflict_resolve(&self, conflict_id: &str, resolved: JsonValue) -> RedDBResult<()> {
379        self.runtime.vcs_conflict_resolve(conflict_id, resolved)
380    }
381
382    pub fn resolve_as_of(&self, spec: AsOfSpec) -> RedDBResult<Xid> {
383        self.runtime.vcs_resolve_as_of(spec)
384    }
385
386    /// Opt a user collection into Git-for-Data. Once flagged, the
387    /// collection participates in merge / diff / AS OF semantics.
388    pub fn set_versioned(&self, collection: &str, enabled: bool) -> RedDBResult<()> {
389        self.runtime.vcs_set_versioned(collection, enabled)
390    }
391
392    /// List every user collection currently opted into VCS.
393    pub fn list_versioned(&self) -> RedDBResult<Vec<String>> {
394        self.runtime.vcs_list_versioned()
395    }
396
397    /// Is this user collection opted in?
398    pub fn is_versioned(&self, collection: &str) -> RedDBResult<bool> {
399        self.runtime.vcs_is_versioned(collection)
400    }
401
402    /// Resolve a short ref / commit prefix / branch / tag to a full
403    /// commit hash. Primary caller is the query parser's AS OF path.
404    pub fn resolve_commitish(&self, spec: &str) -> RedDBResult<CommitHash> {
405        self.runtime.vcs_resolve_commitish(spec)
406    }
407}