Skip to main content

glitter_lang/
git.rs

1use git2;
2use git2::{Branch, BranchType, Repository};
3use std::fmt::Write;
4use std::ops::{AddAssign, BitAnd};
5
6/// Stats which the interpreter uses to populate the gist expression
7#[derive(Debug, PartialEq, Eq, Default, Clone)]
8pub struct Stats {
9    /// Number of untracked files which are new to the repository
10    pub untracked: u16,
11    /// Number of files to be added
12    pub added_staged: u16,
13    /// Number of modified files which have not yet been staged
14    pub modified: u16,
15    /// Number of staged changes to files
16    pub modified_staged: u16,
17    /// Number of renamed files
18    pub renamed: u16,
19    /// Number of deleted files
20    pub deleted: u16,
21    /// Number of staged deletions
22    pub deleted_staged: u16,
23    /// Number of commits ahead of the upstream branch
24    pub ahead: u16,
25    /// Number of commits behind the upstream branch
26    pub behind: u16,
27    /// Number of unresolved conflicts in the repository
28    pub conflicts: u16,
29    /// Number of stashes on the current branch
30    pub stashes: u16,
31    /// The branch name or other stats of the HEAD pointer
32    pub branch: String,
33    /// The of the upstream branch
34    pub remote: String,
35}
36
37impl Stats {
38    /// Populate stats with the status of the given repository
39    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    /// Read the branch-name of the repository
90    ///
91    /// If in detached head, grab the first few characters of the commit ID if possible, otherwise
92    /// simply provide HEAD as the branch name.  This is to mimic the behaviour of `git status`.
93    fn read_branch(&mut self, repo: &Repository) {
94        self.branch = match repo.head() {
95            Ok(head) => {
96                if let Some(name) = head.shorthand() {
97                    // try to use first 8 characters or so of the ID in detached HEAD
98                    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                    // Grab the branch from the reference
109                    } else {
110                        let branch = name.to_string();
111                        // Since we have a branch name, look for the name of the upstream branch
112                        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    /// Read name of the upstream branch
126    fn read_upstream_name(&mut self, repo: &Repository, branch: &str) {
127        // First grab branch from the name
128        self.remote = match repo.find_branch(branch, BranchType::Local) {
129            Ok(branch) => {
130                // Grab the upstream from the branch
131                match branch.upstream() {
132                    // Grab the name of the upstream if it's valid UTF-8
133                    Ok(upstream) => {
134                        // While we have the upstream branch, traverse the graph and count
135                        // ahead-behind commits.
136                        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    /// Read ahead-behind information between the local and upstream branches
151    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/// Check the bits of a flag against the value to see if they are set
178#[inline]
179fn check<B>(val: B, flag: B) -> bool
180where
181    B: BitAnd<Output = B> + PartialEq + Copy,
182{
183    val & flag == flag
184}