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    pull::PullCommand, push::PushCommand, rebase::RebaseCommand, reflog::ReflogCommand,
43    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 [`StashCommand`] scoped to this repository.
245    #[must_use]
246    pub fn stash(&self, action: StashCommand) -> StashCommand {
247        let mut c = action;
248        c.current_dir(&self.path);
249        c
250    }
251
252    /// Build a [`ResetCommand`] scoped to this repository.
253    #[must_use]
254    pub fn reset(&self) -> ResetCommand {
255        let mut c = ResetCommand::new();
256        c.current_dir(&self.path);
257        c
258    }
259
260    /// Build a [`RestoreCommand`] scoped to this repository.
261    #[must_use]
262    pub fn restore(&self) -> RestoreCommand {
263        let mut c = RestoreCommand::new();
264        c.current_dir(&self.path);
265        c
266    }
267
268    /// Build an [`RmCommand`] scoped to this repository.
269    #[must_use]
270    pub fn rm(&self) -> RmCommand {
271        let mut c = RmCommand::new();
272        c.current_dir(&self.path);
273        c
274    }
275
276    /// Build an [`MvCommand`] scoped to this repository.
277    pub fn mv(&self, src: impl Into<String>, dst: impl Into<String>) -> MvCommand {
278        let mut c = MvCommand::new(src, dst);
279        c.current_dir(&self.path);
280        c
281    }
282
283    /// Build a [`CherryPickCommand`] scoped to this repository.
284    #[must_use]
285    pub fn cherry_pick(&self) -> CherryPickCommand {
286        let mut c = CherryPickCommand::new();
287        c.current_dir(&self.path);
288        c
289    }
290
291    /// Build a [`GrepCommand`] scoped to this repository with the given pattern.
292    pub fn grep(&self, pattern: impl Into<String>) -> GrepCommand {
293        let mut c = GrepCommand::new(pattern);
294        c.current_dir(&self.path);
295        c
296    }
297
298    /// Build a [`ConfigCommand`] scoped to this repository.
299    #[must_use]
300    pub fn config(&self, action: ConfigCommand) -> ConfigCommand {
301        let mut c = action;
302        c.current_dir(&self.path);
303        c
304    }
305
306    /// Build a [`ReflogCommand`] scoped to this repository.
307    #[must_use]
308    pub fn reflog(&self, action: ReflogCommand) -> ReflogCommand {
309        let mut c = action;
310        c.current_dir(&self.path);
311        c
312    }
313
314    /// Build a [`WorktreeCommand`] scoped to this repository.
315    #[must_use]
316    pub fn worktree(&self, action: WorktreeCommand) -> WorktreeCommand {
317        let mut c = action;
318        c.current_dir(&self.path);
319        c
320    }
321
322    /// Build a [`SubmoduleCommand`] scoped to this repository.
323    #[must_use]
324    pub fn submodule(&self, action: SubmoduleCommand) -> SubmoduleCommand {
325        let mut c = action;
326        c.current_dir(&self.path);
327        c
328    }
329
330    /// Build a [`BisectCommand`] scoped to this repository.
331    #[must_use]
332    pub fn bisect(&self, action: BisectCommand) -> BisectCommand {
333        let mut c = action;
334        c.current_dir(&self.path);
335        c
336    }
337
338    /// Build a [`RevParseCommand`] scoped to this repository.
339    #[must_use]
340    pub fn rev_parse(&self) -> RevParseCommand {
341        let mut c = RevParseCommand::new();
342        c.current_dir(&self.path);
343        c
344    }
345
346    /// Build a [`DescribeCommand`] scoped to this repository.
347    #[must_use]
348    pub fn describe(&self) -> DescribeCommand {
349        let mut c = DescribeCommand::new();
350        c.current_dir(&self.path);
351        c
352    }
353
354    /// Build an [`LsFilesCommand`] scoped to this repository.
355    #[must_use]
356    pub fn ls_files(&self) -> LsFilesCommand {
357        let mut c = LsFilesCommand::new();
358        c.current_dir(&self.path);
359        c
360    }
361
362    /// Build an [`LsTreeCommand`] for `tree`, scoped to this repository.
363    pub fn ls_tree(&self, tree: impl Into<String>) -> LsTreeCommand {
364        let mut c = LsTreeCommand::new(tree);
365        c.current_dir(&self.path);
366        c
367    }
368
369    /// Build a [`ShowRefCommand`] scoped to this repository.
370    #[must_use]
371    pub fn show_ref(&self) -> ShowRefCommand {
372        let mut c = ShowRefCommand::new();
373        c.current_dir(&self.path);
374        c
375    }
376
377    /// Build a [`SymbolicRefCommand`] scoped to this repository.
378    ///
379    /// Construct `action` with [`SymbolicRefCommand::read`],
380    /// [`SymbolicRefCommand::set`], or [`SymbolicRefCommand::delete`].
381    #[must_use]
382    pub fn symbolic_ref(&self, action: SymbolicRefCommand) -> SymbolicRefCommand {
383        let mut c = action;
384        c.current_dir(&self.path);
385        c
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn open_missing_repo_errors() {
395        let tmp = tempfile::tempdir().unwrap();
396        let err = Repository::open(tmp.path()).unwrap_err();
397        assert!(matches!(err, Error::NotARepository { .. }));
398    }
399
400    #[test]
401    fn new_unchecked_does_not_check() {
402        let repo = Repository::new_unchecked("/definitely/not/here");
403        assert_eq!(repo.path(), Path::new("/definitely/not/here"));
404    }
405}