git_spawn/lib.rs
1//! # git-spawn
2//!
3//! A Rust wrapper around the `git` CLI. Each git subcommand is a struct with a
4//! builder-style API; calling [`.execute().await`](GitCommand::execute) spawns
5//! `git` as a subprocess and returns typed output.
6//!
7//! Unlike libraries that link against libgit2, this crate shells out to the
8//! `git` binary installed on the host. That choice has trade-offs:
9//!
10//! | Pro | Con |
11//! |----------------------------------------------|----------------------------------------|
12//! | Behavior matches the user's local git | Requires `git` on `PATH` at runtime |
13//! | Supports every flag — escape hatches for all | Higher per-call overhead than libgit2 |
14//! | Honors `core.*` config, hooks, credentials | Output parsing is up to you (or us!) |
15//!
16//! If you want a scripting-like experience with the same flags you'd type in a
17//! shell, this crate is for you. For in-process object database manipulation,
18//! reach for [`git2`](https://docs.rs/git2) instead.
19//!
20//! ## Quick start
21//!
22//! ```no_run
23//! use git_spawn::{GitCommand, Repository};
24//!
25//! # async fn example() -> git_spawn::Result<()> {
26//! let repo = Repository::open("/path/to/repo")?;
27//!
28//! // Stage everything and commit.
29//! repo.add().all().execute().await?;
30//! repo.commit().message("snapshot").execute().await?;
31//!
32//! // Push to origin/main.
33//! repo.push().remote("origin").refspec("main").execute().await?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! ## Core concepts
39//!
40//! ### The `GitCommand` trait
41//!
42//! Every command struct implements [`GitCommand`]. That trait provides:
43//!
44//! - [`execute()`](GitCommand::execute) — run the command and decode output
45//! - [`arg()`](GitCommand::arg) / [`args()`](GitCommand::args) — append raw CLI
46//! arguments (the universal escape hatch)
47//! - [`current_dir()`](GitCommand::current_dir) — choose the working directory
48//! - [`env()`](GitCommand::env) — set environment variables
49//! - [`with_timeout()`](GitCommand::with_timeout) — cap execution time
50//!
51//! ### `Repository`
52//!
53//! [`Repository`] is a cheap, cloneable handle to a working tree. Its accessor
54//! methods return commands pre-scoped to that path, so you rarely need to set
55//! `.current_dir()` explicitly:
56//!
57//! ```no_run
58//! # async fn ex() -> git_spawn::Result<()> {
59//! # use git_spawn::{GitCommand, Repository};
60//! let repo = Repository::open("/path/to/repo")?;
61//! let status = repo.status().format(
62//! git_spawn::command::status::StatusFormat::PorcelainV2
63//! ).execute().await?;
64//! println!("{}", status.stdout);
65//! # Ok(())
66//! # }
67//! ```
68//!
69//! ### Typed parsers (the `parse` module)
70//!
71//! By default commands return [`CommandOutput`] — raw stdout/stderr plus the
72//! exit status. For common outputs the [`parse`] module provides structured
73//! types behind the `parse` feature (on by default):
74//!
75//! ```no_run
76//! # async fn ex() -> git_spawn::Result<()> {
77//! # use git_spawn::{GitCommand, Repository};
78//! use git_spawn::command::status::StatusFormat;
79//! use git_spawn::parse::{parse_status, StatusKind};
80//!
81//! let repo = Repository::open("/path/to/repo")?;
82//! let out = repo.status()
83//! .format(StatusFormat::PorcelainV1)
84//! .null_terminate()
85//! .execute()
86//! .await?;
87//! for entry in parse_status(&out.stdout)? {
88//! if entry.index == StatusKind::Modified {
89//! println!("modified in index: {}", entry.path);
90//! }
91//! }
92//! # Ok(())
93//! # }
94//! ```
95//!
96//! ## Feature flags
97//!
98//! | Flag | Default | Purpose |
99//! |------------|:-------:|----------------------------------------------------|
100//! | `parse` | on | Typed parsers for status/log/diff output |
101//! | `serde` | off | `Serialize`/`Deserialize` on parsed types |
102//! | `workflow` | off | Higher-level helpers ([`info`], [`branches`], [`tags`], [`history`], [`workflow`]) |
103//!
104//! ## Error handling
105//!
106//! All methods return [`Result<T>`](Result). The [`Error`] enum distinguishes
107//! common failure modes — `git` missing from `PATH`, a non-zero exit, a
108//! timeout, an invalid builder configuration, a path that isn't a git repo,
109//! and so on. The [`Error::CommandFailed`] variant carries the captured
110//! `stdout`, `stderr`, and exit code so you can present a good error message
111//! or retry.
112//!
113//! ## Design principles
114//!
115//! - **One struct per subcommand** under [`command`]
116//! - **Async-first** on [`tokio`]
117//! - **Raw output by default**; typed parsing is opt-in via the `parse` module
118//! - **Escape hatches everywhere** so the crate is useful for flags we haven't
119//! wrapped yet
120//! - **No unsafe code**, no global state, no hidden config
121
122#![cfg_attr(docsrs, feature(doc_cfg))]
123#![warn(missing_docs)]
124
125pub mod command;
126pub mod error;
127#[cfg(feature = "parse")]
128#[cfg_attr(docsrs, doc(cfg(feature = "parse")))]
129pub mod parse;
130pub mod repo;
131
132#[cfg(feature = "workflow")]
133#[cfg_attr(docsrs, doc(cfg(feature = "workflow")))]
134pub mod branches;
135#[cfg(feature = "workflow")]
136#[cfg_attr(docsrs, doc(cfg(feature = "workflow")))]
137pub mod history;
138#[cfg(feature = "workflow")]
139#[cfg_attr(docsrs, doc(cfg(feature = "workflow")))]
140pub mod info;
141#[cfg(feature = "workflow")]
142#[cfg_attr(docsrs, doc(cfg(feature = "workflow")))]
143pub mod tags;
144#[cfg(feature = "workflow")]
145#[cfg_attr(docsrs, doc(cfg(feature = "workflow")))]
146pub mod workflow;
147
148pub use command::{
149 CommandExecutor, CommandOutput, GitCommand, add::AddCommand, bisect::BisectCommand,
150 branch::BranchCommand, cat_file::CatFileCommand, checkout::CheckoutCommand,
151 cherry_pick::CherryPickCommand, clone::CloneCommand, commit::CommitCommand,
152 config::ConfigCommand, describe::DescribeCommand, diff::DiffCommand, fetch::FetchCommand,
153 for_each_ref::ForEachRefCommand, grep::GrepCommand, hash_object::HashObjectCommand,
154 init::InitCommand, log::LogCommand, ls_files::LsFilesCommand, ls_tree::LsTreeCommand,
155 merge::MergeCommand, mv::MvCommand, pull::PullCommand, push::PushCommand,
156 rebase::RebaseCommand, reflog::ReflogCommand, remote::RemoteCommand, reset::ResetCommand,
157 restore::RestoreCommand, rev_parse::RevParseCommand, rm::RmCommand, show::ShowCommand,
158 show_ref::ShowRefCommand, stash::StashCommand, status::StatusCommand,
159 submodule::SubmoduleCommand, switch::SwitchCommand, symbolic_ref::SymbolicRefCommand,
160 tag::TagCommand, update_ref::UpdateRefCommand, worktree::WorktreeCommand,
161};
162pub use error::{Error, Result};
163pub use repo::Repository;