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, describe::DescribeCommand, diff::DiffCommand,
40    fetch::FetchCommand, grep::GrepCommand, init::InitCommand, log::LogCommand,
41    ls_files::LsFilesCommand, ls_tree::LsTreeCommand, merge::MergeCommand, mv::MvCommand,
42    notes::NotesCommand, pull::PullCommand, push::PushCommand, rebase::RebaseCommand,
43    reflog::ReflogCommand, remote::RemoteCommand, reset::ResetCommand, restore::RestoreCommand,
44    rev_parse::RevParseCommand, rm::RmCommand, show::ShowCommand, show_ref::ShowRefCommand,
45    stash::StashCommand, status::StatusCommand, submodule::SubmoduleCommand, switch::SwitchCommand,
46    symbolic_ref::SymbolicRefCommand, tag::TagCommand, worktree::WorktreeCommand,
47};
48use crate::error::{Error, Result};
49use std::path::{Path, PathBuf};
50
51/// A handle to a git working tree.
52///
53/// Construction does not spawn `git`. [`Repository::open`] only verifies that
54/// a `.git` directory (or file, for worktrees/submodules) exists at the path.
55#[derive(Debug, Clone)]
56pub struct Repository {
57    path: PathBuf,
58}
59
60impl Repository {
61    /// Open an existing repository at `path` without running `git`.
62    ///
63    /// Returns [`Error::NotARepository`] if `path/.git` does not exist.
64    pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
65        let path = path.into();
66        let dotgit = path.join(".git");
67        if !dotgit.exists() {
68            return Err(Error::not_a_repository(path.display().to_string()));
69        }
70        Ok(Self { path })
71    }
72
73    /// Construct a [`Repository`] for `path` without checking that it exists.
74    ///
75    /// Use this when you are about to run `init` or `clone` into the path.
76    #[must_use]
77    pub fn new_unchecked(path: impl Into<PathBuf>) -> Self {
78        Self { path: path.into() }
79    }
80
81    /// Working-tree path.
82    #[must_use]
83    pub fn path(&self) -> &Path {
84        &self.path
85    }
86
87    /// Path to the `.git` directory (or file) inside the working tree.
88    #[must_use]
89    pub fn git_dir(&self) -> PathBuf {
90        self.path.join(".git")
91    }
92
93    /// Initialize a new repository at `path`.
94    ///
95    /// Equivalent to `git init <path>`. Returns the created [`Repository`].
96    pub async fn init(path: impl Into<PathBuf>) -> Result<Self> {
97        let path = path.into();
98        if let Some(parent) = path.parent() {
99            if !parent.as_os_str().is_empty() && !parent.exists() {
100                std::fs::create_dir_all(parent).map_err(Error::from)?;
101            }
102        }
103        if !path.exists() {
104            std::fs::create_dir_all(&path).map_err(Error::from)?;
105        }
106        InitCommand::in_directory(path).execute().await
107    }
108
109    /// Clone `url` into `path`.
110    pub async fn clone(url: impl Into<String>, path: impl Into<PathBuf>) -> Result<Self> {
111        let mut cmd = CloneCommand::new(url);
112        cmd.directory(path);
113        cmd.execute().await
114    }
115
116    /// Build an [`AddCommand`] scoped to this repository.
117    #[must_use]
118    pub fn add(&self) -> AddCommand {
119        let mut c = AddCommand::new();
120        c.current_dir(&self.path);
121        c
122    }
123
124    /// Build a [`CommitCommand`] scoped to this repository.
125    #[must_use]
126    pub fn commit(&self) -> CommitCommand {
127        let mut c = CommitCommand::new();
128        c.current_dir(&self.path);
129        c
130    }
131
132    /// Build a [`StatusCommand`] scoped to this repository.
133    #[must_use]
134    pub fn status(&self) -> StatusCommand {
135        let mut c = StatusCommand::new();
136        c.current_dir(&self.path);
137        c
138    }
139
140    /// Build a [`LogCommand`] scoped to this repository.
141    #[must_use]
142    pub fn log(&self) -> LogCommand {
143        let mut c = LogCommand::new();
144        c.current_dir(&self.path);
145        c
146    }
147
148    /// Build a [`DiffCommand`] scoped to this repository.
149    #[must_use]
150    pub fn diff(&self) -> DiffCommand {
151        let mut c = DiffCommand::new();
152        c.current_dir(&self.path);
153        c
154    }
155
156    /// Build a [`ShowCommand`] scoped to this repository.
157    #[must_use]
158    pub fn show(&self) -> ShowCommand {
159        let mut c = ShowCommand::new();
160        c.current_dir(&self.path);
161        c
162    }
163
164    /// Build a [`BranchCommand`] scoped to this repository.
165    #[must_use]
166    pub fn branch(&self) -> BranchCommand {
167        let mut c = BranchCommand::new();
168        c.current_dir(&self.path);
169        c
170    }
171
172    /// Build a [`CheckoutCommand`] scoped to this repository.
173    #[must_use]
174    pub fn checkout(&self) -> CheckoutCommand {
175        let mut c = CheckoutCommand::new();
176        c.current_dir(&self.path);
177        c
178    }
179
180    /// Build a [`SwitchCommand`] scoped to this repository.
181    #[must_use]
182    pub fn switch(&self) -> SwitchCommand {
183        let mut c = SwitchCommand::new();
184        c.current_dir(&self.path);
185        c
186    }
187
188    /// Build a [`MergeCommand`] scoped to this repository.
189    #[must_use]
190    pub fn merge(&self) -> MergeCommand {
191        let mut c = MergeCommand::new();
192        c.current_dir(&self.path);
193        c
194    }
195
196    /// Build a [`RebaseCommand`] scoped to this repository.
197    #[must_use]
198    pub fn rebase(&self) -> RebaseCommand {
199        let mut c = RebaseCommand::new();
200        c.current_dir(&self.path);
201        c
202    }
203
204    /// Build a [`PullCommand`] scoped to this repository.
205    #[must_use]
206    pub fn pull(&self) -> PullCommand {
207        let mut c = PullCommand::new();
208        c.current_dir(&self.path);
209        c
210    }
211
212    /// Build a [`PushCommand`] scoped to this repository.
213    #[must_use]
214    pub fn push(&self) -> PushCommand {
215        let mut c = PushCommand::new();
216        c.current_dir(&self.path);
217        c
218    }
219
220    /// Build a [`FetchCommand`] scoped to this repository.
221    #[must_use]
222    pub fn fetch(&self) -> FetchCommand {
223        let mut c = FetchCommand::new();
224        c.current_dir(&self.path);
225        c
226    }
227
228    /// Build a [`RemoteCommand`] scoped to this repository.
229    #[must_use]
230    pub fn remote(&self, action: RemoteCommand) -> RemoteCommand {
231        let mut c = action;
232        c.current_dir(&self.path);
233        c
234    }
235
236    /// Build a [`TagCommand`] scoped to this repository.
237    #[must_use]
238    pub fn tag(&self) -> TagCommand {
239        let mut c = TagCommand::new();
240        c.current_dir(&self.path);
241        c
242    }
243
244    /// Build a [`NotesCommand`] scoped to this repository.
245    ///
246    /// Construct `action` with [`NotesCommand::add`], [`NotesCommand::append`],
247    /// [`NotesCommand::copy`], [`NotesCommand::show`], [`NotesCommand::list`],
248    /// [`NotesCommand::remove`], or [`NotesCommand::prune`].
249    #[must_use]
250    pub fn notes(&self, action: NotesCommand) -> NotesCommand {
251        let mut c = action;
252        c.current_dir(&self.path);
253        c
254    }
255
256    /// Build a [`StashCommand`] scoped to this repository.
257    #[must_use]
258    pub fn stash(&self, action: StashCommand) -> StashCommand {
259        let mut c = action;
260        c.current_dir(&self.path);
261        c
262    }
263
264    /// Build a [`ResetCommand`] scoped to this repository.
265    #[must_use]
266    pub fn reset(&self) -> ResetCommand {
267        let mut c = ResetCommand::new();
268        c.current_dir(&self.path);
269        c
270    }
271
272    /// Build a [`RestoreCommand`] scoped to this repository.
273    #[must_use]
274    pub fn restore(&self) -> RestoreCommand {
275        let mut c = RestoreCommand::new();
276        c.current_dir(&self.path);
277        c
278    }
279
280    /// Build an [`RmCommand`] scoped to this repository.
281    #[must_use]
282    pub fn rm(&self) -> RmCommand {
283        let mut c = RmCommand::new();
284        c.current_dir(&self.path);
285        c
286    }
287
288    /// Build an [`MvCommand`] scoped to this repository.
289    pub fn mv(&self, src: impl Into<String>, dst: impl Into<String>) -> MvCommand {
290        let mut c = MvCommand::new(src, dst);
291        c.current_dir(&self.path);
292        c
293    }
294
295    /// Build a [`CherryPickCommand`] scoped to this repository.
296    #[must_use]
297    pub fn cherry_pick(&self) -> CherryPickCommand {
298        let mut c = CherryPickCommand::new();
299        c.current_dir(&self.path);
300        c
301    }
302
303    /// Build a [`GrepCommand`] scoped to this repository with the given pattern.
304    pub fn grep(&self, pattern: impl Into<String>) -> GrepCommand {
305        let mut c = GrepCommand::new(pattern);
306        c.current_dir(&self.path);
307        c
308    }
309
310    /// Build a [`ConfigCommand`] scoped to this repository.
311    #[must_use]
312    pub fn config(&self, action: ConfigCommand) -> ConfigCommand {
313        let mut c = action;
314        c.current_dir(&self.path);
315        c
316    }
317
318    /// Build a [`ReflogCommand`] scoped to this repository.
319    #[must_use]
320    pub fn reflog(&self, action: ReflogCommand) -> ReflogCommand {
321        let mut c = action;
322        c.current_dir(&self.path);
323        c
324    }
325
326    /// Build a [`WorktreeCommand`] scoped to this repository.
327    #[must_use]
328    pub fn worktree(&self, action: WorktreeCommand) -> WorktreeCommand {
329        let mut c = action;
330        c.current_dir(&self.path);
331        c
332    }
333
334    /// Build a [`SubmoduleCommand`] scoped to this repository.
335    #[must_use]
336    pub fn submodule(&self, action: SubmoduleCommand) -> SubmoduleCommand {
337        let mut c = action;
338        c.current_dir(&self.path);
339        c
340    }
341
342    /// Build a [`BisectCommand`] scoped to this repository.
343    #[must_use]
344    pub fn bisect(&self, action: BisectCommand) -> BisectCommand {
345        let mut c = action;
346        c.current_dir(&self.path);
347        c
348    }
349
350    /// Build a [`RevParseCommand`] scoped to this repository.
351    #[must_use]
352    pub fn rev_parse(&self) -> RevParseCommand {
353        let mut c = RevParseCommand::new();
354        c.current_dir(&self.path);
355        c
356    }
357
358    /// Build a [`DescribeCommand`] scoped to this repository.
359    #[must_use]
360    pub fn describe(&self) -> DescribeCommand {
361        let mut c = DescribeCommand::new();
362        c.current_dir(&self.path);
363        c
364    }
365
366    /// Build an [`LsFilesCommand`] scoped to this repository.
367    #[must_use]
368    pub fn ls_files(&self) -> LsFilesCommand {
369        let mut c = LsFilesCommand::new();
370        c.current_dir(&self.path);
371        c
372    }
373
374    /// Build an [`LsTreeCommand`] for `tree`, scoped to this repository.
375    pub fn ls_tree(&self, tree: impl Into<String>) -> LsTreeCommand {
376        let mut c = LsTreeCommand::new(tree);
377        c.current_dir(&self.path);
378        c
379    }
380
381    /// Build a [`ShowRefCommand`] scoped to this repository.
382    #[must_use]
383    pub fn show_ref(&self) -> ShowRefCommand {
384        let mut c = ShowRefCommand::new();
385        c.current_dir(&self.path);
386        c
387    }
388
389    /// Build a [`SymbolicRefCommand`] scoped to this repository.
390    ///
391    /// Construct `action` with [`SymbolicRefCommand::read`],
392    /// [`SymbolicRefCommand::set`], or [`SymbolicRefCommand::delete`].
393    #[must_use]
394    pub fn symbolic_ref(&self, action: SymbolicRefCommand) -> SymbolicRefCommand {
395        let mut c = action;
396        c.current_dir(&self.path);
397        c
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn open_missing_repo_errors() {
407        let tmp = tempfile::tempdir().unwrap();
408        let err = Repository::open(tmp.path()).unwrap_err();
409        assert!(matches!(err, Error::NotARepository { .. }));
410    }
411
412    #[test]
413    fn new_unchecked_does_not_check() {
414        let repo = Repository::new_unchecked("/definitely/not/here");
415        assert_eq!(repo.path(), Path::new("/definitely/not/here"));
416    }
417}