1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
use git2;
use git2::{Branch, BranchType, Repository};
use std::fmt::Write;
use std::ops::{AddAssign, BitAnd};
/// Stats which the interpreter uses to populate the gist expression
#[derive(Debug, PartialEq, Eq, Default, Clone)]
pub struct Stats {
/// Number of untracked files which are new to the repository
pub untracked: u16,
/// Number of files to be added
pub added_staged: u16,
/// Number of modified files which have not yet been staged
pub modified: u16,
/// Number of staged changes to files
pub modified_staged: u16,
/// Number of renamed files
pub renamed: u16,
/// Number of deleted files
pub deleted: u16,
/// Number of staged deletions
pub deleted_staged: u16,
/// Number of commits ahead of the upstream branch
pub ahead: u16,
/// Number of commits behind the upstream branch
pub behind: u16,
/// Number of unresolved conflicts in the repository
pub conflicts: u16,
/// Number of stashes on the current branch
pub stashes: u16,
/// The branch name or other stats of the HEAD pointer
pub branch: String,
/// The of the upstream branch
pub remote: String,
}
impl Stats {
/// Populate stats with the status of the given repository
pub fn new(repo: &mut Repository) -> Stats {
let mut st: Stats = Default::default();
st.read_branch(repo);
let mut opts = git2::StatusOptions::new();
opts.include_untracked(true)
.recurse_untracked_dirs(true)
.renames_head_to_index(true);
if let Ok(statuses) = repo.statuses(Some(&mut opts)) {
for status in statuses.iter() {
let flags = status.status();
if check(flags, git2::Status::WT_NEW) {
st.untracked += 1;
}
if check(flags, git2::Status::INDEX_NEW) {
st.added_staged += 1;
}
if check(flags, git2::Status::WT_MODIFIED) {
st.modified += 1;
}
if check(flags, git2::Status::INDEX_MODIFIED) {
st.modified_staged += 1;
}
if check(flags, git2::Status::INDEX_RENAMED) {
st.renamed += 1;
}
if check(flags, git2::Status::WT_DELETED) {
st.deleted += 1;
}
if check(flags, git2::Status::INDEX_DELETED) {
st.deleted_staged += 1;
}
if check(flags, git2::Status::CONFLICTED) {
st.conflicts += 1;
}
}
}
let _ = repo.stash_foreach(|_, &_, &_| {
st.stashes += 1;
true
});
st
}
/// Read the branch-name of the repository
///
/// If in detached head, grab the first few characters of the commit ID if possible, otherwise
/// simply provide HEAD as the branch name. This is to mimic the behaviour of `git status`.
fn read_branch(&mut self, repo: &Repository) {
self.branch = match repo.head() {
Ok(head) => {
if let Some(name) = head.shorthand() {
// try to use first 8 characters or so of the ID in detached HEAD
if name == "HEAD" {
if let Ok(commit) = head.peel_to_commit() {
let mut id = String::new();
for byte in &commit.id().as_bytes()[..4] {
write!(&mut id, "{:x}", byte).unwrap();
}
id
} else {
"HEAD".to_string()
}
// Grab the branch from the reference
} else {
let branch = name.to_string();
// Since we have a branch name, look for the name of the upstream branch
self.read_upstream_name(repo, &branch);
branch
}
} else {
"HEAD".to_string()
}
}
Err(ref err) if err.code() == git2::ErrorCode::BareRepo => "master".to_string(),
Err(_) if repo.is_empty().unwrap_or(false) => "master".to_string(),
Err(_) => "HEAD".to_string(),
};
}
/// Read name of the upstream branch
fn read_upstream_name(&mut self, repo: &Repository, branch: &str) {
// First grab branch from the name
self.remote = match repo.find_branch(branch, BranchType::Local) {
Ok(branch) => {
// Grab the upstream from the branch
match branch.upstream() {
// Grab the name of the upstream if it's valid UTF-8
Ok(upstream) => {
// While we have the upstream branch, traverse the graph and count
// ahead-behind commits.
self.read_ahead_behind(repo, &branch, &upstream);
match upstream.name() {
Ok(Some(name)) => name.to_string(),
_ => String::new(),
}
}
_ => String::new(),
}
}
_ => String::new(),
};
}
/// Read ahead-behind information between the local and upstream branches
fn read_ahead_behind(&mut self, repo: &Repository, local: &Branch, upstream: &Branch) {
if let (Some(local), Some(upstream)) = (local.get().target(), upstream.get().target()) {
if let Ok((ahead, behind)) = repo.graph_ahead_behind(local, upstream) {
self.ahead = ahead as u16;
self.behind = behind as u16;
}
}
}
}
impl AddAssign for Stats {
fn add_assign(&mut self, rhs: Self) {
self.untracked += rhs.untracked;
self.added_staged += rhs.added_staged;
self.modified += rhs.modified;
self.modified_staged += rhs.modified_staged;
self.renamed += rhs.renamed;
self.deleted += rhs.deleted;
self.deleted_staged += rhs.deleted_staged;
self.ahead += rhs.ahead;
self.behind += rhs.behind;
self.conflicts += rhs.conflicts;
self.stashes += rhs.stashes;
}
}
/// Check the bits of a flag against the value to see if they are set
#[inline]
fn check<B>(val: B, flag: B) -> bool
where
B: BitAnd<Output = B> + PartialEq + Copy,
{
val & flag == flag
}