mhgit/lib.rs
1//! MHgit is a simple git library for interracting with git repositories.
2//!
3//! Interraction with git repositories are done through [`Repository`] objects.
4//!
5//! Simple git commands can be run with the repository methods. For more
6//! complex commands, or to set command options before running, several
7//! _Options_ types are provided.
8//!
9//! ```rust,no_run
10//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
11//! use mhgit::{Repository, CommandOptions};
12//! use mhgit::commands::AddOptions;
13//!
14//! let repo = Repository::new();
15//! AddOptions::new()
16//! .all(true)
17//! .run(&repo)?;
18//! # Ok(())
19//! # }
20//! ```
21//!
22//! Creating repository
23//! -------------------
24//!
25//! ```rust,no_run
26//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
27//! use mhgit::Repository;
28//! Repository::at("/home/mh/awesomeness")?
29//! .init()?
30//! .add()?
31//! .commit("Initial commit")?;
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! [`Repository`]: struct.Repository.html
37
38// Copyright 2020 Magnus Aa. Hirth. All rights reserved.
39
40#![allow(unused_imports, unused_variables, dead_code)]
41
42#[macro_use]
43extern crate failure;
44
45use failure::{Fail, ResultExt};
46use std::convert::TryFrom;
47use std::fmt;
48use std::fs;
49use std::path::{Path, PathBuf};
50use std::process::{self, Command, Stdio, Output};
51
52mod status;
53pub mod commands;
54
55pub use status::Status;
56
57type Result<T> = std::result::Result<T, failure::Error>;
58
59/// Git errors are returned when a git command fails.
60#[derive(Fail, Debug)]
61pub struct GitError {
62 cmd: String,
63 code: Option<i32>,
64 #[cause] stderr: failure::Error,
65}
66
67impl fmt::Display for GitError {
68 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
69 if let Some(code) = self.code {
70 write!(f, "{} returned error code {}", self.cmd, code)
71 } else {
72 write!(f, "{} was stopped...", self.cmd)
73 }
74 }
75}
76
77/// GitOut indicates if git output should be piped or printed.
78#[derive(Debug, PartialEq, Eq, Hash)]
79pub enum GitOut {
80 Print,
81 Pipe,
82}
83
84impl Default for GitOut {
85 fn default() -> Self {
86 GitOut::Pipe
87 }
88}
89
90/// A handle to a git repository.
91///
92/// By creating with [`at`] the repository may be somewhere other than in
93/// current working directory.
94///
95/// ```rust,no_run
96/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
97/// use mhgit::Repository;
98/// Repository::at("/home/mh/awesomeness")?
99/// .init()?
100/// .add()?
101/// .commit("Initial commit")?;
102/// # Ok(())
103/// # }
104/// ```
105///
106/// [`at`]: struct.Repository.html#method.at
107#[derive(Debug, Default, PartialEq, Eq, Hash)]
108pub struct Repository {
109 // Location of repository.
110 location: Option<PathBuf>,
111 stdout: GitOut,
112}
113
114/// Trait implemented by all command option struct ([`CommitOptions`], [`PushOptions`], etc.)
115///
116/// [`CommitOptions`]: commands/struct.CommitOptions.html
117/// [`PushOptions`]: commands/struct.PushOptions.html
118pub trait CommandOptions {
119 type Output;
120
121 /// Return a vector of the arguments passed to git.
122 ///
123 /// The vector contains at least one element, which is the name of the subcommand.
124 fn git_args(&self) -> Vec<&str>;
125
126 /// Parse the captured stdout into an appropriate rust type.
127 fn parse_output(&self, out: &str) -> Result<Self::Output>;
128
129 /// Run the command in the given git repository. Calling git and parsing
130 /// and return the output of the command, if any.
131 ///
132 /// If the git command returns error a GitError is returned.
133 ///
134 /// ```rust,no_run
135 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
136 /// use mhgit::{Repository, CommandOptions};
137 /// use mhgit::commands::AddOptions;
138 /// let repo = Repository::new();
139 /// let _ = AddOptions::new()
140 /// .all(true)
141 /// .run(&repo)?;
142 /// # Ok(())
143 /// # }
144 /// ```
145 ///
146 fn run(&self, repo: &Repository) -> Result<Self::Output> {
147 let args = self.git_args();
148 let out = repo.run(args)?;
149 self.parse_output(&out)
150 }
151}
152
153impl Repository {
154
155 /// Get a repository in the current directory.
156 ///
157 /// ```rust,no_run
158 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
159 /// use mhgit::Repository;
160 /// let status = Repository::new()
161 /// .status()?;
162 /// # Ok(())
163 /// # }
164 /// ```
165 pub fn new() -> Repository {
166 // TODO: check if git is installed on the system
167 Repository {
168 ..Default::default()
169 }
170 }
171
172 /// Get a repository at the given location.
173 ///
174 /// ```rust,no_run
175 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
176 /// use mhgit::Repository;
177 /// Repository::at("/home/mh/awesomeness")?
178 /// .init()?;
179 /// # Ok(())
180 /// # }
181 /// ```
182 pub fn at<P: AsRef<Path>>(path: P) -> Result<Repository> {
183 Ok(Repository {
184 location: Some(
185 fs::canonicalize(path).context("failed to canonicalize repository path")?,
186 ),
187 ..Default::default()
188 })
189 }
190
191 /// Return true if the repository is initialized.
192 pub fn is_init(&self) -> bool {
193 let git_dir = match &self.location {
194 Some(loc) => loc.join(".git"),
195 None => PathBuf::from("./.git"),
196 };
197 git_dir.exists() && git_dir.is_dir()
198 }
199
200 /// Configure if the output of git commands run in this repo should be
201 /// piped or printed to screen.
202 ///
203 /// Piping is default.
204 ///
205 /// ```rust,no_run
206 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
207 /// use mhgit::Repository;
208 /// use mhgit::GitOut::Print;
209 /// Repository::new()
210 /// .gitout(Print)
211 /// .status()?;
212 /// # Ok(())
213 /// # }
214 /// ```
215 pub fn gitout(&mut self, val: GitOut) -> &mut Repository {
216 self.stdout = val;
217 self
218 }
219
220 /// Run `git add` in the repository.
221 ///
222 /// The command is called with the --all option. To call `git add` with
223 /// different options use [`AddOptions`].
224 ///
225 /// [`AddOptions`]: commands/struct.AddOptions.html
226 pub fn add(&mut self) -> Result<&mut Self> {
227 let args = vec!["add", "--all"];
228 self.run(args)?;
229 Ok(self)
230 }
231
232 /// Run `git commit` in the repository, with the given commit message.
233 ///
234 /// The command is called with --allow-empty, avoiding errors if no changes
235 /// were added since last commit. To call `git commit` with different
236 /// options use [`CommitOptions`].
237 ///
238 /// [`CommitOptions`]: commands/struct.CommitOptions.html
239 pub fn commit(&mut self, msg: &str) -> Result<&mut Self> {
240 let args = vec!["commit", "-m", msg, "-q", "--allow-empty"];
241 self.run(args)?;
242 Ok(self)
243 }
244
245 /// Run `git fetch` in the repository.
246 ///
247 /// The command is called with --all
248 pub fn fetch(&mut self) -> Result<&mut Self> {
249 let args = vec!["fetch", "--all", "-q"];
250 self.run(args)?;
251 Ok(self)
252 }
253
254 /// Run `git init`, initializing the repository.
255 pub fn init(&mut self) -> Result<&mut Self> {
256 // Create the directory if it doesn't already exist
257 if let Some(loc) = &self.location {
258 if !loc.exists() {
259 fs::create_dir_all(loc)?;
260 }
261 }
262 let args = vec!["init", "-q"];
263 self.run(args)?;
264 Ok(self)
265 }
266
267 /// Run `git notes add`, adding a note to HEAD.
268 ///
269 /// To call `git notes` with different optinos use [`NotesOptions`].
270 ///
271 /// [`NotesOptions`]: commands/struct.NotesOptions.html
272 pub fn notes(&mut self, msg: &str) -> Result<&mut Self> {
273 let args = vec!["notes", "add", "-m", msg];
274 self.run(args)?;
275 Ok(self)
276 }
277
278 /// Run `git pull` without specifying remote or refs.
279 ///
280 /// To call `git pull` with different options use [`PullOptions`].
281 ///
282 /// [`PullOptions`]: commands/struct.PullOptions.html
283 pub fn pull(&mut self) -> Result<&mut Self> {
284 let args = vec!["pull", "-q"];
285 self.run(args)?;
286 Ok(self)
287 }
288
289 /// Run `git push` without specifying remote or refs.
290 ///
291 /// To call `git push` with different options use [`PushOptions`].
292 ///
293 /// [`PushOptions`]: commands/struct.PushOptions.html
294 pub fn push(&mut self) -> Result<&mut Self> {
295 let args = vec!["push", "-q"];
296 self.run(args)?;
297 Ok(self)
298 }
299
300 /// Run `git remote add` in the repository.
301 ///
302 /// This adds a single remote to the repository. To call `git remote`
303 /// with different options use [`RemoteOptions`].
304 ///
305 /// [`RemoteOptions`]: commands/struct.RemoteOptions.html
306 pub fn remote(&mut self, name: &str, url: &str) -> Result<&mut Self> {
307 let args = vec!["remote", "add", name, url];
308 self.run(args)?;
309 Ok(self)
310 }
311
312 /// Run `git status` parsing the status into idiomatic Rust type.
313 ///
314 /// The status information is returned in a [`Status`].
315 ///
316 /// [`Status`]: struct.Status.html
317 pub fn status(&self) -> Result<Status> {
318 let args = vec!["status", "--porcelain=v2", "--branch", "--ignored"];
319 let out = self.run(args)?;
320 Status::try_from(out.as_str())
321 }
322
323 /// Run `git stash` in the repository.
324 ///
325 /// The command is run without ony options.
326 pub fn stash(&mut self) -> Result<&mut Self> {
327 let args = vec!["stash", "-q"];
328 self.run(args)?;
329 Ok(self)
330 }
331
332 /// Run `git tag`, creating a new tag object.
333 ///
334 /// To call `git tag` with different options use [`TagOptions`].
335 ///
336 /// [`TagOptions`]: commands/struct.TagOptions.html
337 pub fn tag(&mut self, tagname: &str) -> Result<&mut Self> {
338 let args = vec!["tag", tagname];
339 self.run(args)?;
340 Ok(self)
341 }
342
343 fn run(&self, args: Vec<&str>) -> Result<String> {
344 // Setup command
345 let mut cmd = Command::new("git");
346 cmd.stdin(Stdio::inherit());
347 if matches!(self.stdout, GitOut::Print) {
348 cmd.stdout(Stdio::inherit())
349 .stderr(Stdio::inherit());
350 }
351 if let Some(path) = &self.location {
352 (&mut cmd).current_dir(path);
353 }
354 (&mut cmd).args(&args);
355
356 if matches!(self.stdout, GitOut::Print) {
357 // Run with inherited stdin/out
358 let status = cmd.status().context("git execution failed")?;
359 if status.success() {
360 return Ok(String::new())
361 } else {
362 Err(GitError {
363 cmd: format!("git {}", args[0]),
364 code: status.code(),
365 stderr: format_err!("check stderr output"),
366 }.into())
367 }
368 } else {
369 // Run with piped stdin/out
370 let out = cmd.output().context("git execution failed")?;
371 if out.status.success() {
372 Ok(String::from_utf8(out.stdout)?)
373 } else {
374 Err(GitError {
375 cmd: format!("git {}", args[0]),
376 code: out.status.code(),
377 stderr: format_err!("{}", std::str::from_utf8(&out.stderr)?),
378 }.into())
379 }
380 }
381 }
382}
383
384// -----------------------------------------------------------------------------
385// Tests
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 #[test]
391 fn test_mhgit_unit() {
392 assert_eq!(2 + 2, 4);
393 }
394}