Skip to main content

rtb_vcs/git/
mod.rs

1//! Git-operations slice for `rtb-vcs` v0.2 (the v0.5 framework
2//! milestone).
3//!
4//! # Foundation
5//!
6//! This module ships the `Repo` type — a thin, async wrapper over
7//! [`gix`] that downstream tools compose richer git-based behaviour
8//! on top of. Per project memory: `Repo` is a *foundation*, not a
9//! curated facade. v0.5 lays the vocabulary; v0.5.x and later add
10//! capability without breaking the public surface.
11//!
12//! # Backend
13//!
14//! [`gix`] is the primary backend; [`gix::ThreadSafeRepository`] is
15//! the storage type so `Repo: Send + Sync` and can be cloned freely
16//! across `tokio::spawn` boundaries. Every public method wraps a
17//! blocking gix call in [`tokio::task::spawn_blocking`] (per spec
18//! §3.1 + A1 resolution).
19//!
20//! `git2` is an opt-in fallback for operations gix cannot yet do
21//! (push, primarily). Gated on the `git2-fallback` Cargo feature.
22//!
23//! # Auth
24//!
25//! Auth-requiring methods (`clone`, `fetch`, `push`) take a
26//! `&CredentialRef` (already declared by the host tool's typed
27//! config) and resolve through [`rtb_credentials::Resolver`]. See
28//! `crate::git::auth` for the glue and the v0.5 scope spec §3.3 for
29//! the rationale (A2 resolution — no parallel `TokenSource` trait).
30//!
31//! # Error model
32//!
33//! Backend errors are **wrapped, not leaked** (A8). See
34//! [`RepoError`] for the variant table; the internal mapping lives
35//! in `crate::git::error`.
36
37use std::path::{Path, PathBuf};
38
39pub use self::blame::{Blame, BlameLine};
40pub use self::checkout::CheckoutOptions;
41pub use self::clone::CloneOptions;
42pub use self::diff::{ChangeKind, Diff, FileChange};
43pub use self::error::RepoError;
44pub use self::fetch::FetchOptions;
45pub use self::init::InitOptions;
46pub use self::push::PushOptions;
47pub use self::status::RepoStatus;
48pub use self::walk::{CommitInfo, CommitWalk};
49
50pub(crate) mod auth;
51mod blame;
52mod checkout;
53mod clone;
54mod commit;
55mod diff;
56mod error;
57mod fetch;
58mod init;
59mod push;
60mod status;
61mod walk;
62
63/// A repository handle. Cheap to clone — every field is either
64/// `Arc`-wrapped (the gix handle) or a small owned value.
65///
66/// `Repo` is `Send + Sync`; method clones across `tokio::spawn` are
67/// fine. See module-level docs for the wider design.
68#[derive(Debug, Clone)]
69pub struct Repo {
70    /// Thread-safe gix repository handle. Methods that need a
71    /// thread-local view call [`gix::ThreadSafeRepository::to_thread_local`]
72    /// inside their `spawn_blocking` body.
73    inner: gix::ThreadSafeRepository,
74    /// The on-disk path the handle was constructed from. Kept for
75    /// diagnostics (every error variant in [`RepoError`] that has a
76    /// `path` field uses this).
77    path: PathBuf,
78}
79
80impl Repo {
81    /// Open an existing repository at `path`. Discovers `.git` if
82    /// `path` is a subdirectory of a working tree (matches `git`'s
83    /// own discovery rules).
84    ///
85    /// # Errors
86    ///
87    /// - [`RepoError::OpenFailed`] — the path does not contain a
88    ///   repository (or gix cannot read it). `cause` carries the
89    ///   backend's stringified error.
90    pub async fn open(path: impl AsRef<Path>) -> Result<Self, RepoError> {
91        let path: PathBuf = path.as_ref().to_path_buf();
92        let path_for_task = path.clone();
93        tokio::task::spawn_blocking(move || {
94            let repo = gix::open(&path_for_task).map_err(|e| RepoError::OpenFailed {
95                path: path_for_task.clone(),
96                cause: e.to_string(),
97            })?;
98            Ok::<_, RepoError>(Self::from_thread_safe(repo.into_sync(), path_for_task))
99        })
100        .await
101        .map_err(|join_err| RepoError::OpenFailed {
102            path: path.clone(),
103            cause: format!("spawn_blocking join error: {join_err}"),
104        })?
105    }
106
107    /// Construct a `Repo` from an existing gix thread-safe handle +
108    /// the on-disk path it was opened from. Used by `init` / `open`
109    /// after the gix-side handle has been obtained.
110    pub(crate) const fn from_thread_safe(inner: gix::ThreadSafeRepository, path: PathBuf) -> Self {
111        Self { inner, path }
112    }
113
114    /// The on-disk path this `Repo` was opened / initialised from.
115    /// Stable across the lifetime of the handle.
116    #[must_use]
117    pub fn path(&self) -> &Path {
118        &self.path
119    }
120
121    /// Clone the underlying thread-safe gix handle. Used by methods
122    /// to move a handle into a `spawn_blocking` body without keeping
123    /// `&self` alive across the await.
124    pub(crate) fn thread_safe(&self) -> gix::ThreadSafeRepository {
125        self.inner.clone()
126    }
127}