1use std::fmt;
4use std::path::PathBuf;
5
6use serde::Serialize;
7
8#[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 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 pub fn path(&self) -> String {
34 self.path.to_str().unwrap().to_string()
35 }
36
37 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 fn branch_to_commit(branch: git2::Branch) -> Option<git2::Commit> {
59 branch.into_reference().peel_to_commit().ok()
60 }
61
62 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 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 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
121fn 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 format!("{}{}", istatus, wstatus)
156}