1use git2;
2use git2::{Branch, BranchType, Repository};
3use std::fmt::Write;
4use std::ops::{AddAssign, BitAnd};
5
6#[derive(Debug, PartialEq, Eq, Default, Clone)]
8pub struct Stats {
9 pub untracked: u16,
11 pub added_staged: u16,
13 pub modified: u16,
15 pub modified_staged: u16,
17 pub renamed: u16,
19 pub deleted: u16,
21 pub deleted_staged: u16,
23 pub ahead: u16,
25 pub behind: u16,
27 pub conflicts: u16,
29 pub stashes: u16,
31 pub branch: String,
33 pub remote: String,
35}
36
37impl Stats {
38 pub fn new(repo: &mut Repository) -> Stats {
40 let mut st: Stats = Default::default();
41
42 st.read_branch(repo);
43
44 let mut opts = git2::StatusOptions::new();
45
46 opts.include_untracked(true)
47 .recurse_untracked_dirs(true)
48 .renames_head_to_index(true);
49
50 if let Ok(statuses) = repo.statuses(Some(&mut opts)) {
51 for status in statuses.iter() {
52 let flags = status.status();
53
54 if check(flags, git2::Status::WT_NEW) {
55 st.untracked += 1;
56 }
57 if check(flags, git2::Status::INDEX_NEW) {
58 st.added_staged += 1;
59 }
60 if check(flags, git2::Status::WT_MODIFIED) {
61 st.modified += 1;
62 }
63 if check(flags, git2::Status::INDEX_MODIFIED) {
64 st.modified_staged += 1;
65 }
66 if check(flags, git2::Status::INDEX_RENAMED) {
67 st.renamed += 1;
68 }
69 if check(flags, git2::Status::WT_DELETED) {
70 st.deleted += 1;
71 }
72 if check(flags, git2::Status::INDEX_DELETED) {
73 st.deleted_staged += 1;
74 }
75 if check(flags, git2::Status::CONFLICTED) {
76 st.conflicts += 1;
77 }
78 }
79 }
80
81 let _ = repo.stash_foreach(|_, &_, &_| {
82 st.stashes += 1;
83 true
84 });
85
86 st
87 }
88
89 fn read_branch(&mut self, repo: &Repository) {
94 self.branch = match repo.head() {
95 Ok(head) => {
96 if let Some(name) = head.shorthand() {
97 if name == "HEAD" {
99 if let Ok(commit) = head.peel_to_commit() {
100 let mut id = String::new();
101 for byte in &commit.id().as_bytes()[..4] {
102 write!(&mut id, "{:x}", byte).unwrap();
103 }
104 id
105 } else {
106 "HEAD".to_string()
107 }
108 } else {
110 let branch = name.to_string();
111 self.read_upstream_name(repo, &branch);
113 branch
114 }
115 } else {
116 "HEAD".to_string()
117 }
118 }
119 Err(ref err) if err.code() == git2::ErrorCode::BareRepo => "master".to_string(),
120 Err(_) if repo.is_empty().unwrap_or(false) => "master".to_string(),
121 Err(_) => "HEAD".to_string(),
122 };
123 }
124
125 fn read_upstream_name(&mut self, repo: &Repository, branch: &str) {
127 self.remote = match repo.find_branch(branch, BranchType::Local) {
129 Ok(branch) => {
130 match branch.upstream() {
132 Ok(upstream) => {
134 self.read_ahead_behind(repo, &branch, &upstream);
137
138 match upstream.name() {
139 Ok(Some(name)) => name.to_string(),
140 _ => String::new(),
141 }
142 }
143 _ => String::new(),
144 }
145 }
146 _ => String::new(),
147 };
148 }
149
150 fn read_ahead_behind(&mut self, repo: &Repository, local: &Branch, upstream: &Branch) {
152 if let (Some(local), Some(upstream)) = (local.get().target(), upstream.get().target()) {
153 if let Ok((ahead, behind)) = repo.graph_ahead_behind(local, upstream) {
154 self.ahead = ahead as u16;
155 self.behind = behind as u16;
156 }
157 }
158 }
159}
160
161impl AddAssign for Stats {
162 fn add_assign(&mut self, rhs: Self) {
163 self.untracked += rhs.untracked;
164 self.added_staged += rhs.added_staged;
165 self.modified += rhs.modified;
166 self.modified_staged += rhs.modified_staged;
167 self.renamed += rhs.renamed;
168 self.deleted += rhs.deleted;
169 self.deleted_staged += rhs.deleted_staged;
170 self.ahead += rhs.ahead;
171 self.behind += rhs.behind;
172 self.conflicts += rhs.conflicts;
173 self.stashes += rhs.stashes;
174 }
175}
176
177#[inline]
179fn check<B>(val: B, flag: B) -> bool
180where
181 B: BitAnd<Output = B> + PartialEq + Copy,
182{
183 val & flag == flag
184}