Skip to main content

endringer_core/
backend.rs

1//! The [`VcsBackend`] trait that all VCS backends implement.
2//!
3//! This trait is `pub` so that downstream crates can implement custom
4//! backends and inject them via [`endringer::repository::Repository::with_backend`].
5//!
6//! # Stability
7//!
8//! **Before v1.0, this trait is implementable but not fully stable.**
9//! New required methods may still be added; however, any method that has a
10//! truthful default implementation will be given one, so that adding it does
11//! not break existing custom backends. Methods without a default are the
12//! *required core methods* listed first in the trait below.
13//!
14//! Consumers that depend only on [`endringer::repository::Repository`]
15//! receive stronger stability guarantees than consumers that implement
16//! `VcsBackend` directly.
17//!
18//! # Method categories
19//!
20//! | Category | Default | Notes |
21//! |---|---|---|
22//! | Required core | none | must be implemented; no safe default exists |
23//! | Optional-empty | `Ok(vec![])` or `None` | backend may have no such data |
24//! | Write-side exception | unsupported error | tags are the only in-scope writes |
25
26use std::time::SystemTime;
27
28use crate::error::Result;
29use crate::types::{
30    AheadBehind, BlameEntry, BranchInfo, BranchTrackingInfo, CommitId, CommitInfo,
31    CommitQuery, CommitQueryResult,
32    ConflictSummary, DiffEntry, DiffOptions, DiffSummary,
33    OperationState, RefInfo, RefKind, RemoteInfo,
34    RepositoryInfo, RepositorySnapshot, RichWorktreeStatus, SnapshotRequest,
35    SortOrder, StashDetail, StashEntry, StatusDigest, StatusOptions,
36    SubmoduleInfo, SubmoduleSummary, TagInfo, TreeEntry,
37    WorktreeDetail, WorktreeInfo, WorktreeStatus,
38};
39
40/// Common interface implemented by every VCS backend.
41///
42/// All methods take `&self` and are safe to call concurrently from multiple
43/// threads (`Send + Sync` bound).
44///
45/// See the [module documentation][self] for the stability policy and method
46/// classification.
47pub trait VcsBackend: Send + Sync {
48    // ── Required core methods ──────────────────────────────────────────── //
49    //
50    // No defaults: a safe stand-in would be misleading or wrong.
51
52    fn status_digest(&self) -> Result<StatusDigest>;
53
54    fn local_branches(&self) -> Result<Vec<BranchInfo>>;
55    fn remote_branches(&self) -> Result<Vec<BranchInfo>>;
56
57    fn list_commits(&self) -> Result<Vec<CommitInfo>>;
58    fn list_commits_sorted(&self, order: SortOrder) -> Result<Vec<CommitInfo>>;
59    fn log_since(&self, since: SystemTime, until: SystemTime) -> Result<Vec<CommitInfo>>;
60    fn find_commit(&self, id: &CommitId) -> Result<CommitInfo>;
61
62    /// Returns a bounded page of commit history according to `query`.
63    ///
64    /// `CommitQueryResult::truncated` is `true` when `max_count` was reached
65    /// and more commits may exist beyond the page.
66    ///
67    /// Default: unsupported-feature error.
68    fn query_commits(&self, _query: CommitQuery) -> Result<CommitQueryResult> {
69        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "query_commits" })
70    }
71
72    fn list_tags(&self) -> Result<Vec<TagInfo>>;
73    fn list_tags_sorted(&self, order: SortOrder) -> Result<Vec<TagInfo>>;
74
75    fn diff(&self, from: &CommitId, to: &CommitId) -> Result<DiffSummary>;
76
77    /// Returns `true` if the working tree has any uncommitted changes
78    /// (staged or unstaged). Bare repositories always return `false`.
79    fn is_dirty(&self) -> Result<bool>;
80
81    /// Returns the best common ancestor of `a` and `b`, or `None` if there
82    /// is no shared history (unrelated histories).
83    fn merge_base(&self, a: &CommitId, b: &CommitId) -> Result<Option<CommitId>>;
84
85    /// Returns `true` if `candidate` is a direct or transitive ancestor of
86    /// `descendant`. A commit is considered its own ancestor.
87    fn is_ancestor(&self, candidate: &CommitId, descendant: &CommitId) -> Result<bool>;
88
89    /// Returns per-line commit attribution for `path` at HEAD.
90    ///
91    /// `path` is relative to the repository root.
92    /// Entries are in ascending line order.
93    fn blame(&self, path: &std::path::Path) -> Result<Vec<BlameEntry>>;
94
95    /// Returns per-file working-tree status: staged changes, unstaged
96    /// changes, and untracked files (with gitignore applied).
97    fn worktree_status(&self) -> Result<WorktreeStatus>;
98
99    /// Returns a richer working-tree status with more file-level change kinds.
100    ///
101    /// Default: unsupported-feature error.
102    fn rich_worktree_status(&self, _options: StatusOptions) -> Result<RichWorktreeStatus> {
103        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "rich_worktree_status" })
104    }
105
106    /// Returns the raw bytes of `path` (relative to the repository root)
107    /// as it exists in the tree of `commit_id`.
108    fn file_at_commit(&self, path: &std::path::Path, commit_id: &CommitId) -> Result<Vec<u8>>;
109
110    /// Returns ahead/behind counts between `local` and `upstream` commit tips.
111    ///
112    /// See [`AheadBehind`] for the full contract and edge cases.
113    fn ahead_behind(&self, local: &CommitId, upstream: &CommitId) -> Result<AheadBehind>;
114
115    /// Returns a lightweight metadata snapshot of the repository.
116    ///
117    /// Includes backend kind, working tree path, HEAD state, object format,
118    /// and backend capabilities. All fields are a fresh read; this is not a
119    /// subscription.
120    fn repository_info(&self) -> Result<RepositoryInfo>;
121
122    // ── Optional-empty methods ─────────────────────────────────────────── //
123    //
124    // Returning empty is semantically valid when the backend has no such data.
125
126    /// Returns the fetch URL of the named remote, or `Ok(None)` if not configured.
127    ///
128    /// Returns `Err` only on an actual I/O or config parsing failure.
129    ///
130    /// Default: `Ok(None)`.
131    fn remote_url(&self, _name: &str) -> Result<Option<String>> {
132        Ok(None)
133    }
134
135    /// Returns metadata for all submodules declared in `.gitmodules`.
136    ///
137    /// Default: empty `Vec`.
138    fn submodules(&self) -> Result<Vec<SubmoduleInfo>> {
139        Ok(Vec::new())
140    }
141
142    /// Returns all stash entries, newest first.
143    ///
144    /// Default: empty `Vec`.
145    fn stash_entries(&self) -> Result<Vec<StashEntry>> {
146        Ok(Vec::new())
147    }
148
149    /// Returns all linked worktrees. The main worktree is not included.
150    ///
151    /// Default: empty `Vec`.
152    fn worktrees(&self) -> Result<Vec<WorktreeInfo>> {
153        Ok(Vec::new())
154    }
155
156    // ── Write-side exception methods ───────────────────────────────────── //
157    //
158    // Tags are the only in-scope writes. Custom backends that do not support
159    // tag operations receive unsupported-feature errors by default.
160
161    /// Creates a lightweight tag at HEAD.
162    ///
163    /// Default: unsupported-feature error.
164    fn create_tag(&self, _name: &str) -> Result<()> {
165        return Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "create_tag" })
166    }
167
168    /// Creates an annotated tag at HEAD.
169    ///
170    /// Requires `user.name` and `user.email` in git config. On the jj
171    /// backend this returns an unsupported error; use
172    /// [`create_tag`][VcsBackend::create_tag] instead.
173    ///
174    /// Default: unsupported-feature error.
175    fn create_annotated_tag(&self, _name: &str, _message: &str) -> Result<()> {
176        return Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "create_annotated_tag" })
177    }
178
179    /// Deletes the named tag.
180    ///
181    /// Default: unsupported-feature error.
182    fn delete_tag(&self, _name: &str) -> Result<()> {
183        return Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "delete_tag" })
184    }
185
186    // ── Optional-unsupported methods ───────────────────────────────────── //
187    //
188    // These have a truthful default but backends should override where possible.
189
190    /// Returns tracking metadata and divergence data for `branch`.
191    ///
192    /// Default: unsupported-feature error.
193    fn branch_tracking(&self, _branch: &str) -> Result<BranchTrackingInfo> {
194        return Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "branch_tracking" })
195    }
196
197    /// Returns tracking metadata for all local branches, sorted ascending
198    /// by full ref name.
199    ///
200    /// Default: unsupported-feature error.
201    fn local_branch_tracking(&self) -> Result<Vec<BranchTrackingInfo>> {
202        return Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "local_branch_tracking" })
203    }
204
205    /// Returns `true` if `branch` has been merged into `target`.
206    ///
207    /// Equivalent to `is_ancestor(branch_tip, target_tip)` but with named
208    /// branches, preventing callers from accidentally reversing the arguments.
209    ///
210    /// Default: unsupported-feature error.
211    fn is_merged_into(&self, _branch: &str, _target: &str) -> Result<bool> {
212        Err(crate::error::Error::UnsupportedBackendFeature {
213            backend: None,
214            feature: "is_merged_into",
215        })
216    }
217
218    /// Returns ahead/behind counts for the configured upstream of `branch`.
219    ///
220    /// Returns `Ok(None)` when the branch has no configured upstream.
221    /// Returns an error when the configured upstream ref no longer exists
222    /// locally.
223    ///
224    /// Default: unsupported-feature error.
225    fn branch_ahead_behind(&self, _branch: &str) -> Result<Option<AheadBehind>> {
226        return Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "branch_ahead_behind" })
227    }
228
229    // ── Operation and conflict state (RFC 008) ─────────────────────────── //
230
231    /// Returns the current in-progress repository operation, if any.
232    ///
233    /// Reads Git marker files (`MERGE_HEAD`, `rebase-merge/`, `rebase-apply/`,
234    /// `CHERRY_PICK_HEAD`, `REVERT_HEAD`, `BISECT_LOG`, `refs/bisect/`).
235    ///
236    /// Returns `Ok(OperationState::None)` when no operation is in progress.
237    ///
238    /// Default: unsupported-feature error.
239    fn operation_state(&self) -> Result<OperationState> {
240        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "operation_state" })
241    }
242
243    /// Returns paths with unmerged (higher-stage) index entries.
244    ///
245    /// Returns a sorted, deduplicated list. Empty when no conflicts exist.
246    ///
247    /// Default: unsupported-feature error.
248    fn unmerged_paths(&self) -> Result<Vec<std::path::PathBuf>> {
249        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "unmerged_paths" })
250    }
251
252    /// Returns a structured summary of all conflicted index entries.
253    ///
254    /// Includes per-stage object IDs. For a lighter-weight check,
255    /// prefer [`unmerged_paths`][Self::unmerged_paths].
256    ///
257    /// Default: unsupported-feature error.
258    fn conflict_summary(&self) -> Result<ConflictSummary> {
259        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "conflict_summary" })
260    }
261
262    // ── Point-in-time reads (RFC 010) ──────────────────────────────────── //
263
264    /// Per-line commit attribution for `path` at `commit_id` (not HEAD).
265    ///
266    /// Default: unsupported-feature error.
267    fn blame_at(&self, _path: &std::path::Path, _commit_id: &CommitId) -> Result<Vec<BlameEntry>> {
268        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "blame_at" })
269    }
270
271    /// Non-recursive root-level tree listing at `commit_id`, sorted ascending by name.
272    ///
273    /// Default: unsupported-feature error.
274    fn tree_at_commit(&self, _commit_id: &CommitId) -> Result<Vec<TreeEntry>> {
275        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "tree_at_commit" })
276    }
277
278    /// Non-recursive tree listing of the directory at `path` within `commit_id`.
279    ///
280    /// Returns `Err(PathNotFound)` if `path` does not exist or is not a directory.
281    /// Entries sorted ascending by name.
282    ///
283    /// Default: unsupported-feature error.
284    fn tree_at_path(&self, _commit_id: &CommitId, _path: &std::path::Path) -> Result<Vec<TreeEntry>> {
285        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "tree_at_path" })
286    }
287
288    // ── Remote and reference inventory (RFC 011) ──────────────────────── //
289
290    /// Returns all configured remotes, sorted ascending by name.
291    ///
292    /// Default: unsupported-feature error.
293    fn remotes(&self) -> Result<Vec<RemoteInfo>> {
294        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "remotes" })
295    }
296
297    /// Returns all references, sorted ascending by full name.
298    ///
299    /// Default: unsupported-feature error.
300    fn references(&self) -> Result<Vec<RefInfo>> {
301        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "references" })
302    }
303
304    /// Returns references matching `kind`, sorted ascending by full name.
305    ///
306    /// Default: unsupported-feature error.
307    fn references_by_kind(&self, _kind: RefKind) -> Result<Vec<RefInfo>> {
308        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "references_by_kind" })
309    }
310
311    // ── Submodule detail (RFC 019) ─────────────────────────────────────── //
312
313    /// Returns rich metadata for each submodule, including initialization
314    /// and sync state. More expensive than [`submodules`][Self::submodules].
315    /// Sorted ascending by path.
316    ///
317    /// Default: unsupported-feature error.
318    fn submodule_summaries(&self) -> Result<Vec<SubmoduleSummary>> {
319        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "submodule_summaries" })
320    }
321
322    // ── Stash detail (RFC 020) ─────────────────────────────────────────── //
323
324    /// Returns detailed metadata for the stash entry at `index`.
325    ///
326    /// Default: unsupported-feature error.
327    fn stash_detail(&self, _index: usize) -> Result<StashDetail> {
328        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "stash_detail" })
329    }
330
331    /// Returns a diff summary for the stash at `index` vs its first parent.
332    ///
333    /// Default: unsupported-feature error.
334    fn stash_diff(&self, _index: usize) -> Result<DiffSummary> {
335        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "stash_diff" })
336    }
337
338    // ── Worktree detail (RFC 021) ──────────────────────────────────────── //
339
340    /// Returns rich detail for all linked worktrees, sorted by id.
341    /// Missing worktrees are reported as `WorktreeState::MissingPath` rather
342    /// than omitted.
343    ///
344    /// Default: unsupported-feature error.
345    fn worktree_details(&self) -> Result<Vec<WorktreeDetail>> {
346        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "worktree_details" })
347    }
348
349    // ── Snapshot batch read (RFC 027) ──────────────────────────────────── //
350
351    /// Returns a batch of related reads collected in one method call.
352    ///
353    /// Reduces inter-call drift for status-widget data. Not an atomic
354    /// snapshot — concurrent mutation can still produce a mixed view.
355    ///
356    /// Default: calls each included method sequentially.
357    fn snapshot(&self, request: SnapshotRequest) -> Result<RepositorySnapshot> {
358        let info = self.repository_info()?;
359        let status_digest = if request.include_status_digest {
360            Some(self.status_digest()?)
361        } else { None };
362        let operation_state = if request.include_operation_state {
363            self.operation_state().ok()
364        } else { None };
365        let local_branches = if request.include_local_branches {
366            Some(self.local_branches()?)
367        } else { None };
368        let tags = if request.include_tags {
369            Some(self.list_tags()?)
370        } else { None };
371        Ok(RepositorySnapshot { info, status_digest, operation_state, local_branches, tags })
372    }
373
374    // ── Rename/copy-aware diff (RFC 028) ──────────────────────────────── //
375
376    /// Returns a rename/copy-aware diff between `from` and `to`.
377    ///
378    /// When `options.detect_renames` is `false` (default), the result is
379    /// equivalent to `diff()` expressed as `DiffEntry` values. Rename
380    /// detection is opt-in because it can be expensive.
381    ///
382    /// Default: unsupported-feature error.
383    fn diff_entries(
384        &self,
385        _from: &CommitId,
386        _to: &CommitId,
387        _options: DiffOptions,
388    ) -> Result<Vec<DiffEntry>> {
389        Err(crate::error::Error::UnsupportedBackendFeature { backend: None, feature: "diff_entries" })
390    }
391}