Skip to main content

rtb_forge/git/
mod.rs

1//! Git-operations slice for `rtb-forge` 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//! Write paths (`commit`, `checkout`, `fetch`, `push`, and an
21//! authenticated `clone`) shell out to the `git` binary, which must be
22//! on `PATH` at runtime. `libgit2` is not used.
23//!
24//! # Auth
25//!
26//! Auth-requiring methods (`clone`, `fetch`, `push`) take a
27//! `&CredentialRef` (already declared by the host tool's typed
28//! config) and resolve through [`rtb_credentials::Resolver`]. See
29//! `crate::git::auth` for the glue and the v0.5 scope spec §3.3 for
30//! the rationale (A2 resolution — no parallel `TokenSource` trait).
31//!
32//! # Error model
33//!
34//! Backend errors are **wrapped, not leaked** (A8). See
35//! [`RepoError`] for the variants; each operation stringifies its
36//! backend error into the variant's `cause` field.
37
38use std::path::{Path, PathBuf};
39
40pub use self::blame::{Blame, BlameLine};
41pub use self::checkout::CheckoutOptions;
42pub use self::clone::CloneOptions;
43pub use self::diff::{ChangeKind, Diff, FileChange};
44pub use self::error::RepoError;
45pub use self::fetch::FetchOptions;
46pub use self::init::InitOptions;
47pub use self::push::PushOptions;
48pub use self::status::RepoStatus;
49pub use self::walk::{CommitInfo, CommitWalk};
50
51pub(crate) mod auth;
52mod blame;
53mod checkout;
54mod clone;
55mod commit;
56mod diff;
57mod error;
58mod fetch;
59mod init;
60mod push;
61mod status;
62mod walk;
63
64/// A repository handle. Cheap to clone — every field is either
65/// `Arc`-wrapped (the gix handle) or a small owned value.
66///
67/// `Repo` is `Send + Sync`; method clones across `tokio::spawn` are
68/// fine. See module-level docs for the wider design.
69#[derive(Debug, Clone)]
70pub struct Repo {
71    /// Thread-safe gix repository handle. Methods that need a
72    /// thread-local view call [`gix::ThreadSafeRepository::to_thread_local`]
73    /// inside their `spawn_blocking` body.
74    inner: gix::ThreadSafeRepository,
75    /// The on-disk path the handle was constructed from. Kept for
76    /// diagnostics (every error variant in [`RepoError`] that has a
77    /// `path` field uses this).
78    path: PathBuf,
79}
80
81impl Repo {
82    /// Open an existing repository at `path`. Discovers `.git` if
83    /// `path` is a subdirectory of a working tree (matches `git`'s
84    /// own discovery rules).
85    ///
86    /// # Errors
87    ///
88    /// - [`RepoError::OpenFailed`] — the path does not contain a
89    ///   repository (or gix cannot read it). `cause` carries the
90    ///   backend's stringified error.
91    pub async fn open(path: impl AsRef<Path>) -> Result<Self, RepoError> {
92        let path: PathBuf = path.as_ref().to_path_buf();
93        let path_for_task = path.clone();
94        tokio::task::spawn_blocking(move || {
95            let repo = gix::open(&path_for_task).map_err(|e| RepoError::OpenFailed {
96                path: path_for_task.clone(),
97                cause: e.to_string(),
98            })?;
99            Ok::<_, RepoError>(Self::from_thread_safe(repo.into_sync(), path_for_task))
100        })
101        .await
102        .map_err(|join_err| RepoError::OpenFailed {
103            path: path.clone(),
104            cause: format!("spawn_blocking join error: {join_err}"),
105        })?
106    }
107
108    /// Construct a `Repo` from an existing gix thread-safe handle +
109    /// the on-disk path it was opened from. Used by `init` / `open`
110    /// after the gix-side handle has been obtained.
111    pub(crate) const fn from_thread_safe(inner: gix::ThreadSafeRepository, path: PathBuf) -> Self {
112        Self { inner, path }
113    }
114
115    /// The on-disk path this `Repo` was opened / initialised from.
116    /// Stable across the lifetime of the handle.
117    #[must_use]
118    pub fn path(&self) -> &Path {
119        &self.path
120    }
121
122    /// Clone the underlying thread-safe gix handle. Used by methods
123    /// to move a handle into a `spawn_blocking` body without keeping
124    /// `&self` alive across the await.
125    pub(crate) fn thread_safe(&self) -> gix::ThreadSafeRepository {
126        self.inner.clone()
127    }
128}