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
|
//! This module contains the [`crate::status::Status`] type.
use git2::{ErrorCode, Reference, Remote, Repository, StatusOptions};
use log::debug;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[allow(missing_docs)]
#[derive(Error, Debug)]
pub enum StatusError {
#[error(transparent)]
FromGit2(#[from] git2::Error),
}
#[allow(missing_docs)]
pub type StatusResult<T> = Result<T, StatusError>;
/// A summarized interpretation of the status of a Git working tree.
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Status {
/// Corresponds to a "bare" working tree.
Bare,
/// Corresponds to a "clean" working tree.
Clean,
/// Corresponds to an "unclean" working tree.
Unclean,
/// Provided if the state of the working tree could neither be found nor determined.
Unknown,
/// Indicates that there is at least one commit not pushed to the remote from the working tree.
Unpushed,
}
impl Status {
/// Converts the enum into a borrowed, static `str`.
pub fn as_str(&self) -> &'static str {
match self {
Self::Bare => "bare",
Self::Clean => "clean",
Self::Unclean => "unclean",
Self::Unknown => "unknown",
Self::Unpushed => "unpushed",
}
}
/// Find the [`Status`] for a given [`Repository`](git2::Repository). The
/// [`head`](Option<git2::Reference>) and [`remote`](Option<git2::Remote>) are also returned.
pub fn find(
repo: &Repository,
) -> StatusResult<(Status, Option<Reference<'_>>, Option<Remote<'_>>)> {
let head = match repo.head() {
Ok(head) => Some(head),
Err(ref e)
if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound =>
{
None
}
Err(e) => return Err(e.into()),
};
// Greedily chooses a remote if "origin" is not found.
let (remote, remote_name) = match repo.find_remote("origin") {
Ok(origin) => (Some(origin), Some("origin".to_string())),
Err(e) if e.code() == ErrorCode::NotFound => Self::choose_remote_greedily(repo)?,
Err(e) => return Err(e.into()),
};
// We'll include all untracked files and directories in the status options.
let mut opts = StatusOptions::new();
opts.include_untracked(true).recurse_untracked_dirs(true);
// If "head" is "None" and statuses are empty, then the repository_view must be clean because there
// are no commits to push.
let status = match repo.statuses(Some(&mut opts)) {
Ok(v) if v.is_empty() => match &head {
Some(head) => match remote_name {
Some(remote_name) => match Self::is_unpushed(repo, head, &remote_name)? {
true => Status::Unpushed,
false => Status::Clean,
},
None => Status::Clean,
},
None => Status::Clean,
},
Ok(_) => Status::Unclean,
Err(e) if e.code() == ErrorCode::BareRepo => Status::Bare,
Err(e) => return Err(e.into()),
};
Ok((status, head, remote))
}
// Checks if local commit(s) on the current branch have not yet been pushed to the remote.
fn is_unpushed(
repo: &Repository,
head: &Reference<'_>,
remote_name: &str,
) -> Result<bool, git2::Error> {
let local_head = head.peel_to_commit()?;
let remote = format!(
"{}/{}",
remote_name,
match head.shorthand() {
Some(v) => v,
None => {
debug!("assuming unpushed; could not determine shorthand for head");
return Ok(true);
}
}
);
let remote_head = match repo.resolve_reference_from_short_name(&remote) {
Ok(reference) => reference.peel_to_commit()?,
Err(e) => {
debug!("assuming unpushed; could not resolve remote reference from short name (ignored error: {})", e);
return Ok(true);
}
};
Ok(
matches!(repo.graph_ahead_behind(local_head.id(), remote_head.id()), Ok(number_unique_commits) if number_unique_commits.0 > 0),
)
}
fn choose_remote_greedily(
repository: &Repository,
) -> Result<(Option<Remote<'_>>, Option<String>), git2::Error> {
let remotes = repository.remotes()?;
Ok(match remotes.get(0) {
Some(remote_name) => (
Some(repository.find_remote(remote_name)?),
Some(remote_name.to_string()),
),
None => (None, None),
})
}
}
|