Skip to main content

clgit/
repository.rs

1use crate::*;
2
3use std::fmt::{self, Debug, Formatter};
4use std::ffi::OsStr;
5use std::io::{self, Read};
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::sync::Arc;
9
10
11
12/// A git repository (a reference to a local path containing a .git directory, or a bare some_repository.git directory)
13#[derive(Clone)]
14pub struct Repository {
15    dot_git:    Arc<PathBuf>,
16}
17
18impl Repository {
19    /// # Examples
20    ///
21    /// ```rust
22    /// let repository = clgit::Repository::from_bare_repository(".git").unwrap();
23    /// ```
24    pub fn from_bare_repository(dir: impl Into<PathBuf>) -> io::Result<Self> {
25        // TODO: canonicalize?
26        let dir = dir.into();
27        if dir.join(".git").exists() { return Err(io::Error::new(io::ErrorKind::InvalidData, "not a bare repository")); }
28        Ok(Self { dot_git: Arc::new(dir) })
29    }
30
31    /// # Examples
32    ///
33    /// ```rust
34    /// let repository = clgit::Repository::from_regular_repository(".").unwrap();
35    /// ```
36    pub fn from_regular_repository(dir: impl AsRef<Path>) -> io::Result<Self> {
37        // TODO: canonicalize?
38        let dir = dir.as_ref();
39        let dot_git = dir.join(".git");
40        if !dot_git.exists() { return Err(io::Error::new(io::ErrorKind::InvalidData, "not a regular repository")); }
41        Ok(Self { dot_git: Arc::new(dot_git) })
42    }
43
44
45    /// # Examples
46    ///
47    /// ```rust
48    /// let repository = clgit::Repository::from_path(".").unwrap();
49    /// let repository = clgit::Repository::from_path(".git").unwrap();
50    /// ```
51    pub fn from_path(dir: impl AsRef<Path>) -> io::Result<Self> {
52        let dir = dir.as_ref();
53        let dot_git = dir.join(".git");
54        if dot_git.exists() {
55            Ok(Self { dot_git: Arc::new(dot_git) })
56        } else {
57            Self::from_bare_repository(dir)
58        }
59    }
60
61    /// # Examples
62    ///
63    /// ```rust
64    /// # fn examples() -> std::io::Result<()> {
65    /// # let repository = clgit::Repository::from_regular_repository(".")?;
66    /// for branch in repository.local_branches()? {
67    ///     let branch : clgit::Branch = branch?;
68    ///     println!("{}", branch.name().to_string_lossy());
69    ///     let _ = branch.commit();
70    /// }
71    /// # Ok(())
72    /// # }
73    /// # examples().unwrap()
74    /// ```
75    pub fn local_branches(&self) -> io::Result<impl Iterator<Item = io::Result<Branch>>> {
76        let mut branches = Default::default();
77        gather_branches(OsStr::new(""), &self.dot_git.join("refs/heads"), &mut branches)?;
78        Ok(branches.into_iter().map(|(name, commit)| Ok(Branch { name, commit })))
79    }
80
81    /// # Examples
82    ///
83    /// ```rust
84    /// # fn examples() -> std::io::Result<()> {
85    /// # let repository = clgit::Repository::from_regular_repository(".")?;
86    /// for branch in repository.remote_branches()? {
87    ///     let branch : clgit::Branch = branch?;
88    ///     println!("{}", branch.name().to_string_lossy());
89    ///     let _ = branch.commit();
90    /// }
91    /// # Ok(())
92    /// # }
93    /// # examples().unwrap()
94    /// ```
95    pub fn remote_branches(&self) -> io::Result<impl Iterator<Item = io::Result<Branch>>> {
96        let mut branches = Default::default();
97        gather_branches(OsStr::new(""), &self.dot_git.join("refs/remotes"), &mut branches)?;
98        Ok(branches.into_iter().map(|(name, commit)| Ok(Branch { name, commit })))
99    }
100
101    /// Run/parse `git cat-file -s [hash]`
102    pub fn cat_file_size(&self, hash: &blob::Hash) -> io::Result<u64> {
103        let hash = HashTempStr::new(hash);
104        let git = self.git().args(&["cat-file", "-s", hash.as_str()]).output()?;
105        match git.status.code() {
106            Some(0) => {},
107            Some(_) => return Err(io::Error::new(io::ErrorKind::Other, "git cat-file -s ... exited non-zero")),
108            None    => return Err(io::Error::new(io::ErrorKind::Other, "git cat-file -s ... died by signal")),
109        }
110        Ok(String::from_utf8(git.stdout)
111            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "git cat-file -s ... returned non-utf8 size"))?
112            .trim()
113            .parse()
114            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "git cat-file -s ... returned non-u64 file size"))?
115        )
116    }
117
118    /// Run/parse `git cat-file -t [hash]`
119    pub fn cat_file_type(&self, hash: &unknown::Hash) -> io::Result<FileType> {
120        let hash = HashTempStr::new(hash);
121        let git = self.git().args(&["cat-file", "-t", hash.as_str()]).output()?;
122        match git.status.code() {
123            Some(0) => {},
124            Some(_) => return Err(io::Error::new(io::ErrorKind::Other, "git cat-file -t ... exited non-zero")),
125            None    => return Err(io::Error::new(io::ErrorKind::Other, "git cat-file -t ... died by signal")),
126        }
127        Ok(String::from_utf8(git.stdout).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "git cat-file -t ... returned non-utf8 type"))?.trim().into())
128    }
129
130    /// Run/parse `git cat-file commit [hash]`
131    pub fn cat_file_commit  (&self, hash: &commit::Hash) -> io::Result<impl Read> { self.cat_file("commit", hash) }
132
133    /// Run/parse `git cat-file tree [hash]`
134    pub fn cat_file_tree    (&self, hash: &tree::Hash) -> io::Result<impl Read> { self.cat_file("tree", hash) }
135
136    /// Run/parse `git cat-file blob [hash]`
137    pub fn cat_file_blob    (&self, hash: &blob::Hash) -> io::Result<impl Read> { self.cat_file("blob", hash) }
138
139    fn git(&self) -> Command {
140        let mut c = Command::new("git");
141        c.current_dir(&*self.dot_git);
142        c
143    }
144
145    fn cat_file<T>(&self, ty: &str, hash: &generic::Hash<T>) -> io::Result<impl Read> {
146        let hash = HashTempStr::new(hash);
147        let mut git = self.git()
148            .args(&["cat-file", ty, hash.as_str()])
149            .stdin (Stdio::null())
150            .stderr(Stdio::null())
151            .stdout(Stdio::piped())
152            .spawn()?;
153        Ok(CatFileReader { stdout: git.stdout.take().unwrap(), child: git })
154    }
155}
156
157impl Debug for Repository {
158    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
159        fmt.debug_struct("Repository")
160            .field("dot_git", &self.dot_git)
161            .field("local_branches",    &self.local_branches().map(|b| b.collect::<Vec<_>>()))
162            .field("remote_branches",   &self.remote_branches().map(|r| r.collect::<Vec<_>>()))
163            .finish()
164    }
165}