Skip to main content

git_spawn/
repo.rs

1//! High-level handle for operating on a git repository.
2//!
3//! A [`Repository`] is a cheap, cloneable reference to a working tree path.
4//! It is the entry point for most users: construct one via
5//! [`Repository::open`], [`Repository::init`], or [`Repository::clone`], then
6//! call the accessor methods ([`Repository::add`], [`Repository::commit`],
7//! [`Repository::log`], ...) to build commands pre-scoped to this repo.
8//!
9//! ```no_run
10//! use git_spawn::{GitCommand, Repository};
11//!
12//! # async fn example() -> git_spawn::Result<()> {
13//! // Create a fresh repo and commit a file into it.
14//! let repo = Repository::init("/tmp/demo").await?;
15//! std::fs::write(repo.path().join("hello.txt"), "hi")?;
16//! repo.add().path("hello.txt").execute().await?;
17//! repo.commit().message("first").execute().await?;
18//! # Ok(())
19//! # }
20//! ```
21//!
22//! # Cloning an existing repo
23//!
24//! ```no_run
25//! # use git_spawn::Repository;
26//! # async fn example() -> git_spawn::Result<()> {
27//! let repo = Repository::clone(
28//!     "https://github.com/octocat/Hello-World.git",
29//!     "/tmp/hello-world",
30//! ).await?;
31//! assert!(repo.git_dir().exists());
32//! # Ok(())
33//! # }
34//! ```
35
36use crate::command::{
37    GitCommand, add::AddCommand, bisect::BisectCommand, branch::BranchCommand,
38    checkout::CheckoutCommand, cherry_pick::CherryPickCommand, clone::CloneCommand,
39    commit::CommitCommand, config::ConfigCommand, diff::DiffCommand, fetch::FetchCommand,
40    grep::GrepCommand, init::InitCommand, log::LogCommand, merge::MergeCommand, mv::MvCommand,
41    pull::PullCommand, push::PushCommand, rebase::RebaseCommand, reflog::ReflogCommand,
42    remote::RemoteCommand, reset::ResetCommand, restore::RestoreCommand, rm::RmCommand,
43    show::ShowCommand, stash::StashCommand, status::StatusCommand, submodule::SubmoduleCommand,
44    switch::SwitchCommand, tag::TagCommand, worktree::WorktreeCommand,
45};
46use crate::error::{Error, Result};
47use std::path::{Path, PathBuf};
48
49/// A handle to a git working tree.
50///
51/// Construction does not spawn `git`. [`Repository::open`] only verifies that
52/// a `.git` directory (or file, for worktrees/submodules) exists at the path.
53#[derive(Debug, Clone)]
54pub struct Repository {
55    path: PathBuf,
56}
57
58impl Repository {
59    /// Open an existing repository at `path` without running `git`.
60    ///
61    /// Returns [`Error::NotARepository`] if `path/.git` does not exist.
62    pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
63        let path = path.into();
64        let dotgit = path.join(".git");
65        if !dotgit.exists() {
66            return Err(Error::not_a_repository(path.display().to_string()));
67        }
68        Ok(Self { path })
69    }
70
71    /// Construct a [`Repository`] for `path` without checking that it exists.
72    ///
73    /// Use this when you are about to run `init` or `clone` into the path.
74    #[must_use]
75    pub fn new_unchecked(path: impl Into<PathBuf>) -> Self {
76        Self { path: path.into() }
77    }
78
79    /// Working-tree path.
80    #[must_use]
81    pub fn path(&self) -> &Path {
82        &self.path
83    }
84
85    /// Path to the `.git` directory (or file) inside the working tree.
86    #[must_use]
87    pub fn git_dir(&self) -> PathBuf {
88        self.path.join(".git")
89    }
90
91    /// Initialize a new repository at `path`.
92    ///
93    /// Equivalent to `git init <path>`. Returns the created [`Repository`].
94    pub async fn init(path: impl Into<PathBuf>) -> Result<Self> {
95        let path = path.into();
96        if let Some(parent) = path.parent() {
97            if !parent.as_os_str().is_empty() && !parent.exists() {
98                std::fs::create_dir_all(parent).map_err(Error::from)?;
99            }
100        }
101        if !path.exists() {
102            std::fs::create_dir_all(&path).map_err(Error::from)?;
103        }
104        InitCommand::in_directory(path).execute().await
105    }
106
107    /// Clone `url` into `path`.
108    pub async fn clone(url: impl Into<String>, path: impl Into<PathBuf>) -> Result<Self> {
109        let mut cmd = CloneCommand::new(url);
110        cmd.directory(path);
111        cmd.execute().await
112    }
113
114    /// Build an [`AddCommand`] scoped to this repository.
115    #[must_use]
116    pub fn add(&self) -> AddCommand {
117        let mut c = AddCommand::new();
118        c.current_dir(&self.path);
119        c
120    }
121
122    /// Build a [`CommitCommand`] scoped to this repository.
123    #[must_use]
124    pub fn commit(&self) -> CommitCommand {
125        let mut c = CommitCommand::new();
126        c.current_dir(&self.path);
127        c
128    }
129
130    /// Build a [`StatusCommand`] scoped to this repository.
131    #[must_use]
132    pub fn status(&self) -> StatusCommand {
133        let mut c = StatusCommand::new();
134        c.current_dir(&self.path);
135        c
136    }
137
138    /// Build a [`LogCommand`] scoped to this repository.
139    #[must_use]
140    pub fn log(&self) -> LogCommand {
141        let mut c = LogCommand::new();
142        c.current_dir(&self.path);
143        c
144    }
145
146    /// Build a [`DiffCommand`] scoped to this repository.
147    #[must_use]
148    pub fn diff(&self) -> DiffCommand {
149        let mut c = DiffCommand::new();
150        c.current_dir(&self.path);
151        c
152    }
153
154    /// Build a [`ShowCommand`] scoped to this repository.
155    #[must_use]
156    pub fn show(&self) -> ShowCommand {
157        let mut c = ShowCommand::new();
158        c.current_dir(&self.path);
159        c
160    }
161
162    /// Build a [`BranchCommand`] scoped to this repository.
163    #[must_use]
164    pub fn branch(&self) -> BranchCommand {
165        let mut c = BranchCommand::new();
166        c.current_dir(&self.path);
167        c
168    }
169
170    /// Build a [`CheckoutCommand`] scoped to this repository.
171    #[must_use]
172    pub fn checkout(&self) -> CheckoutCommand {
173        let mut c = CheckoutCommand::new();
174        c.current_dir(&self.path);
175        c
176    }
177
178    /// Build a [`SwitchCommand`] scoped to this repository.
179    #[must_use]
180    pub fn switch(&self) -> SwitchCommand {
181        let mut c = SwitchCommand::new();
182        c.current_dir(&self.path);
183        c
184    }
185
186    /// Build a [`MergeCommand`] scoped to this repository.
187    #[must_use]
188    pub fn merge(&self) -> MergeCommand {
189        let mut c = MergeCommand::new();
190        c.current_dir(&self.path);
191        c
192    }
193
194    /// Build a [`RebaseCommand`] scoped to this repository.
195    #[must_use]
196    pub fn rebase(&self) -> RebaseCommand {
197        let mut c = RebaseCommand::new();
198        c.current_dir(&self.path);
199        c
200    }
201
202    /// Build a [`PullCommand`] scoped to this repository.
203    #[must_use]
204    pub fn pull(&self) -> PullCommand {
205        let mut c = PullCommand::new();
206        c.current_dir(&self.path);
207        c
208    }
209
210    /// Build a [`PushCommand`] scoped to this repository.
211    #[must_use]
212    pub fn push(&self) -> PushCommand {
213        let mut c = PushCommand::new();
214        c.current_dir(&self.path);
215        c
216    }
217
218    /// Build a [`FetchCommand`] scoped to this repository.
219    #[must_use]
220    pub fn fetch(&self) -> FetchCommand {
221        let mut c = FetchCommand::new();
222        c.current_dir(&self.path);
223        c
224    }
225
226    /// Build a [`RemoteCommand`] scoped to this repository.
227    #[must_use]
228    pub fn remote(&self, action: RemoteCommand) -> RemoteCommand {
229        let mut c = action;
230        c.current_dir(&self.path);
231        c
232    }
233
234    /// Build a [`TagCommand`] scoped to this repository.
235    #[must_use]
236    pub fn tag(&self) -> TagCommand {
237        let mut c = TagCommand::new();
238        c.current_dir(&self.path);
239        c
240    }
241
242    /// Build a [`StashCommand`] scoped to this repository.
243    #[must_use]
244    pub fn stash(&self, action: StashCommand) -> StashCommand {
245        let mut c = action;
246        c.current_dir(&self.path);
247        c
248    }
249
250    /// Build a [`ResetCommand`] scoped to this repository.
251    #[must_use]
252    pub fn reset(&self) -> ResetCommand {
253        let mut c = ResetCommand::new();
254        c.current_dir(&self.path);
255        c
256    }
257
258    /// Build a [`RestoreCommand`] scoped to this repository.
259    #[must_use]
260    pub fn restore(&self) -> RestoreCommand {
261        let mut c = RestoreCommand::new();
262        c.current_dir(&self.path);
263        c
264    }
265
266    /// Build an [`RmCommand`] scoped to this repository.
267    #[must_use]
268    pub fn rm(&self) -> RmCommand {
269        let mut c = RmCommand::new();
270        c.current_dir(&self.path);
271        c
272    }
273
274    /// Build an [`MvCommand`] scoped to this repository.
275    pub fn mv(&self, src: impl Into<String>, dst: impl Into<String>) -> MvCommand {
276        let mut c = MvCommand::new(src, dst);
277        c.current_dir(&self.path);
278        c
279    }
280
281    /// Build a [`CherryPickCommand`] scoped to this repository.
282    #[must_use]
283    pub fn cherry_pick(&self) -> CherryPickCommand {
284        let mut c = CherryPickCommand::new();
285        c.current_dir(&self.path);
286        c
287    }
288
289    /// Build a [`GrepCommand`] scoped to this repository with the given pattern.
290    pub fn grep(&self, pattern: impl Into<String>) -> GrepCommand {
291        let mut c = GrepCommand::new(pattern);
292        c.current_dir(&self.path);
293        c
294    }
295
296    /// Build a [`ConfigCommand`] scoped to this repository.
297    #[must_use]
298    pub fn config(&self, action: ConfigCommand) -> ConfigCommand {
299        let mut c = action;
300        c.current_dir(&self.path);
301        c
302    }
303
304    /// Build a [`ReflogCommand`] scoped to this repository.
305    #[must_use]
306    pub fn reflog(&self, action: ReflogCommand) -> ReflogCommand {
307        let mut c = action;
308        c.current_dir(&self.path);
309        c
310    }
311
312    /// Build a [`WorktreeCommand`] scoped to this repository.
313    #[must_use]
314    pub fn worktree(&self, action: WorktreeCommand) -> WorktreeCommand {
315        let mut c = action;
316        c.current_dir(&self.path);
317        c
318    }
319
320    /// Build a [`SubmoduleCommand`] scoped to this repository.
321    #[must_use]
322    pub fn submodule(&self, action: SubmoduleCommand) -> SubmoduleCommand {
323        let mut c = action;
324        c.current_dir(&self.path);
325        c
326    }
327
328    /// Build a [`BisectCommand`] scoped to this repository.
329    #[must_use]
330    pub fn bisect(&self, action: BisectCommand) -> BisectCommand {
331        let mut c = action;
332        c.current_dir(&self.path);
333        c
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn open_missing_repo_errors() {
343        let tmp = tempfile::tempdir().unwrap();
344        let err = Repository::open(tmp.path()).unwrap_err();
345        assert!(matches!(err, Error::NotARepository { .. }));
346    }
347
348    #[test]
349    fn new_unchecked_does_not_check() {
350        let repo = Repository::new_unchecked("/definitely/not/here");
351        assert_eq!(repo.path(), Path::new("/definitely/not/here"));
352    }
353}