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(|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 pub fn path(&self) -> String {
35 self.path.to_str().unwrap().to_string()
36 }
37
38 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 fn branch_to_commit(branch: git2::Branch) -> Option<git2::Commit> {
60 branch.into_reference().peel_to_commit().ok()
61 }
62
63 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 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 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
122fn 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 format!("{}{}", istatus, wstatus)
157}