Skip to main content

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}