Skip to main content

git_global/
repo.rs

1//! Git repository representation for git-global.
2
3use std::fmt;
4use std::path::PathBuf;
5
6use serde::Serialize;
7
8/// A git repository, represented by the full path to its base directory.
9#[derive(Clone, Eq, Hash, PartialEq, Serialize)]
10pub struct Repo {
11    path: PathBuf,
12}
13
14impl Repo {
15    pub fn new(path: String) -> Repo {
16        Repo {
17            path: PathBuf::from(path),
18        }
19    }
20
21    /// Returns the `git2::Repository` equivalent of this repo.
22    pub fn as_git2_repo(&self) -> ::git2::Repository {
23        ::git2::Repository::open(&self.path).unwrap_or_else(|e| {
24            panic!(
25                "Could not open {} as a git repo: {:?}. Perhaps you should run \
26                   `git global scan` again.",
27                &self.path.as_path().to_str().unwrap(),
28                e
29            )
30        })
31    }
32
33    /// Returns the full path to the repo as a `String`.
34    pub fn path(&self) -> String {
35        self.path.to_str().unwrap().to_string()
36    }
37
38    /// Returns "short format" status output.
39    pub fn get_status_lines(
40        &self,
41        mut status_opts: ::git2::StatusOptions,
42    ) -> Vec<String> {
43        let git2_repo = self.as_git2_repo();
44        let statuses = git2_repo
45            .statuses(Some(&mut status_opts))
46            .unwrap_or_else(|_| panic!("Could not get statuses for {}.", self));
47        statuses
48            .iter()
49            .map(|entry| {
50                let path = entry.path().unwrap();
51                let status = entry.status();
52                let status_for_path = get_short_format_status(status);
53                format!("{} {}", status_for_path, path)
54            })
55            .collect()
56    }
57
58    /// Transforms a git2::Branch into a git2::Commit
59    fn branch_to_commit(branch: git2::Branch) -> Option<git2::Commit> {
60        branch.into_reference().peel_to_commit().ok()
61    }
62
63    /// Walks through revisions, returning all ancestor Oids of a Commit
64    fn get_log(
65        repo: &git2::Repository,
66        commit: git2::Commit,
67    ) -> Vec<git2::Oid> {
68        let mut revwalk = repo.revwalk().unwrap();
69        revwalk.push(commit.id()).unwrap();
70        revwalk.filter_map(|id| id.ok()).collect::<Vec<git2::Oid>>()
71    }
72
73    /// Returns true if commits of local branches are ahead of those on remote branches
74    pub fn is_ahead(&self) -> bool {
75        let repo = self.as_git2_repo();
76        let local_branches = match repo.branches(Some(git2::BranchType::Local))
77        {
78            Ok(branches) => branches,
79            Err(_) => return false,
80        };
81        let remote_branches =
82            match repo.branches(Some(git2::BranchType::Remote)) {
83                Ok(branches) => branches,
84                Err(_) => return false,
85            };
86
87        let remote_commit_ids = remote_branches
88            .filter_map(|branch| branch.ok().map(|b| b.0))
89            .filter_map(Self::branch_to_commit)
90            .flat_map(|commit| Self::get_log(&repo, commit))
91            .collect::<Vec<_>>();
92
93        #[allow(clippy::let_and_return)]
94        let is_ahead = local_branches
95            .filter_map(|branch| branch.ok().map(|b| b.0))
96            .any(|branch| match Self::branch_to_commit(branch) {
97                Some(commit) => !remote_commit_ids.contains(&commit.id()),
98                None => false,
99            });
100        is_ahead
101    }
102
103    /// Returns the list of stash entries for the repo.
104    pub fn get_stash_list(&self) -> Vec<String> {
105        let mut stash = vec![];
106        self.as_git2_repo()
107            .stash_foreach(|index, name, _oid| {
108                stash.push(format!("stash@{{{}}}: {}", index, name));
109                true
110            })
111            .unwrap();
112        stash
113    }
114}
115
116impl fmt::Display for Repo {
117    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118        write!(f, "{}", self.path())
119    }
120}
121
122/// Translates a file's status flags to their "short format" representation.
123///
124/// Follows an example in the git2-rs crate's `examples/status.rs`.
125fn get_short_format_status(status: ::git2::Status) -> String {
126    let mut istatus = match status {
127        s if s.is_index_new() => 'A',
128        s if s.is_index_modified() => 'M',
129        s if s.is_index_deleted() => 'D',
130        s if s.is_index_renamed() => 'R',
131        s if s.is_index_typechange() => 'T',
132        _ => ' ',
133    };
134    let mut wstatus = match status {
135        s if s.is_wt_new() => {
136            if istatus == ' ' {
137                istatus = '?';
138            }
139            '?'
140        }
141        s if s.is_wt_modified() => 'M',
142        s if s.is_wt_deleted() => 'D',
143        s if s.is_wt_renamed() => 'R',
144        s if s.is_wt_typechange() => 'T',
145        _ => ' ',
146    };
147    if status.is_ignored() {
148        istatus = '!';
149        wstatus = '!';
150    }
151    if status.is_conflicted() {
152        istatus = 'C';
153        wstatus = 'C';
154    }
155    // TODO: handle submodule statuses?
156    format!("{}{}", istatus, wstatus)
157}