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