use std::fmt;
use std::path::PathBuf;
use serde::Serialize;
#[derive(Clone, Eq, Hash, PartialEq, Serialize)]
pub struct Repo {
path: PathBuf,
}
impl Repo {
pub fn new<P: Into<PathBuf>>(path: P) -> Repo {
Repo {
path: path.into(),
}
}
pub fn as_git2_repo(&self) -> ::git2::Repository {
::git2::Repository::open(&self.path).unwrap_or_else(|e| {
panic!(
"Could not open {} as a git repo: {:?}. Perhaps you should run \
`git global scan` again.",
self.path.as_path().to_str().unwrap(),
e
)
})
}
pub fn path(&self) -> String {
self.path.to_str().unwrap().to_string()
}
pub fn get_status_lines(
&self,
mut status_opts: ::git2::StatusOptions,
) -> Vec<String> {
let git2_repo = self.as_git2_repo();
let statuses = git2_repo
.statuses(Some(&mut status_opts))
.unwrap_or_else(|_| panic!("Could not get statuses for {}.", self));
statuses
.iter()
.map(|entry| {
let path = entry.path().unwrap();
let status = entry.status();
let status_for_path = get_short_format_status(status);
format!("{} {}", status_for_path, path)
})
.collect()
}
fn branch_to_commit(branch: git2::Branch) -> Option<git2::Commit> {
branch.into_reference().peel_to_commit().ok()
}
fn get_log(
repo: &git2::Repository,
commit: git2::Commit,
) -> Vec<git2::Oid> {
let mut revwalk = repo.revwalk().unwrap();
revwalk.push(commit.id()).unwrap();
revwalk.filter_map(|id| id.ok()).collect::<Vec<git2::Oid>>()
}
pub fn is_ahead(&self) -> bool {
let repo = self.as_git2_repo();
let local_branches = match repo.branches(Some(git2::BranchType::Local))
{
Ok(branches) => branches,
Err(_) => return false,
};
let remote_branches =
match repo.branches(Some(git2::BranchType::Remote)) {
Ok(branches) => branches,
Err(_) => return false,
};
let remote_commit_ids = remote_branches
.filter_map(|branch| branch.ok().map(|b| b.0))
.filter_map(Self::branch_to_commit)
.flat_map(|commit| Self::get_log(&repo, commit))
.collect::<Vec<_>>();
#[allow(clippy::let_and_return)]
let is_ahead = local_branches
.filter_map(|branch| branch.ok().map(|b| b.0))
.any(|branch| match Self::branch_to_commit(branch) {
Some(commit) => !remote_commit_ids.contains(&commit.id()),
None => false,
});
is_ahead
}
pub fn get_stash_list(&self) -> Vec<String> {
let mut stash = vec![];
self.as_git2_repo()
.stash_foreach(|index, name, _oid| {
stash.push(format!("stash@{{{}}}: {}", index, name));
true
})
.unwrap();
stash
}
}
impl fmt::Display for Repo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.path())
}
}
fn get_short_format_status(status: ::git2::Status) -> String {
let mut istatus = match status {
s if s.is_index_new() => 'A',
s if s.is_index_modified() => 'M',
s if s.is_index_deleted() => 'D',
s if s.is_index_renamed() => 'R',
s if s.is_index_typechange() => 'T',
_ => ' ',
};
let mut wstatus = match status {
s if s.is_wt_new() => {
if istatus == ' ' {
istatus = '?';
}
'?'
}
s if s.is_wt_modified() => 'M',
s if s.is_wt_deleted() => 'D',
s if s.is_wt_renamed() => 'R',
s if s.is_wt_typechange() => 'T',
_ => ' ',
};
if status.is_ignored() {
istatus = '!';
wstatus = '!';
}
if status.is_conflicted() {
istatus = 'C';
wstatus = 'C';
}
format!("{}{}", istatus, wstatus)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestEnv;
#[test]
fn test_is_ahead() {
let mut env = TestEnv::new();
let repo_path = env.tempdir.path().join("repo_ahead");
let repo = Repo::new(repo_path);
env.create_repo("repo_ahead")
.commit("file.txt", "content")
.setup_remote()
.build();
assert!(!repo.is_ahead());
env.create_repo("repo_ahead")
.commit("file.txt", "new content")
.build();
assert!(repo.is_ahead());
}
#[test]
fn test_get_status_lines() {
let mut env = TestEnv::new();
let repo_path = env.tempdir.path().join("repo_status");
let repo = Repo::new(repo_path);
env.create_repo("repo_status")
.commit("initial.txt", "content")
.stage("staged.txt", "new file")
.unstaged("unstaged.txt", "untracked file")
.build();
let mut opts = git2::StatusOptions::new();
opts.include_untracked(true);
let lines = repo.get_status_lines(opts);
assert!(lines.iter().any(|l| l.starts_with("A staged.txt")));
assert!(lines.iter().any(|l| l.starts_with("?? unstaged.txt")));
}
#[test]
fn test_get_stash_list() {
let mut env = TestEnv::new();
let repo_path = env.tempdir.path().join("repo_stash");
let repo = Repo::new(repo_path);
env.create_repo("repo_stash")
.commit("file.txt", "content")
.unstaged("file.txt", "modified")
.stash("stash 1")
.unstaged("file.txt", "modified again")
.stash("stash 2")
.build();
let stashes = repo.get_stash_list();
assert_eq!(stashes.len(), 2);
assert!(stashes[0].contains("stash 2"));
assert!(stashes[1].contains("stash 1"));
}
}