use std::{
path::{self},
process::Command,
};
use git2::{Branch, Repository, StatusOptions};
use crate::gitinfo::status::Status;
pub mod repoinfo;
pub mod status;
fn get_remote_name(repo: &Repository) -> Option<String> {
if repo.find_remote("origin").is_ok() {
return Some("origin".to_owned());
}
repo.remotes()
.ok()
.and_then(|remotes| remotes.get(0).map(ToOwned::to_owned))
}
fn get_repo_path(repo: &Repository) -> path::PathBuf {
if let Some(workdir) = repo.workdir() {
return workdir.to_path_buf();
}
let path = repo.path();
if path.ends_with(".git") {
path.parent().unwrap_or(path).to_path_buf()
} else {
path.to_path_buf()
}
}
fn get_repo_name(repo: &Repository) -> Option<String> {
let remote_name = get_remote_name(repo)?;
if let Ok(remote) = repo.find_remote(&remote_name)
&& let Some(url) = remote.url()
{
{
return Some(
url.trim_end_matches(".git")
.split('/')
.next_back()
.unwrap_or("unknown")
.to_owned(),
);
}
}
None
}
pub fn get_branch_name(repo: &Repository) -> String {
if let Ok(head) = repo.head() {
if head.is_branch() {
if let Some(name) = head.shorthand() {
return name.to_owned();
}
} else {
return "N/A".to_owned();
}
if let Some(target) = head.symbolic_target()
&& let Some(branch) = target.rsplit('/').next()
{
return format!("{branch} (no commits)");
}
} else if let Ok(headref) = repo.find_reference("HEAD")
&& let Some(sym) = headref.symbolic_target()
&& let Some(branch) = sym.rsplit('/').next()
{
return format!("{branch} (no commits)");
}
"(no branch)".to_owned()
}
pub fn get_ahead_behind_and_local_status(repo: &Repository) -> (usize, usize, bool) {
let Ok(head) = repo.head() else {
return (0, 0, true);
};
let branch = head.shorthand().map_or_else(
|| None,
|name| repo.find_branch(name, git2::BranchType::Local).ok(),
);
if let Some(branch) = branch
&& let Ok(upstream) = branch.upstream()
{
let local_oid = branch.get().target();
let upstream_oid = upstream.get().target();
if let (Some(local), Some(up)) = (local_oid, upstream_oid) {
let (ahead, behind) = repo.graph_ahead_behind(local, up).unwrap_or((0, 0));
return (ahead, behind, false);
}
}
(0, 0, true)
}
pub fn get_total_commits(repo: &Repository) -> anyhow::Result<usize> {
let Ok(head) = repo.head() else { return Ok(0) };
let Some(oid) = head.target() else {
return Ok(0);
};
let mut revwalk = repo.revwalk()?;
revwalk.push(oid)?;
Ok(revwalk.count())
}
pub fn get_changed_count(repo: &Repository) -> usize {
let mut opts = StatusOptions::new();
opts.include_untracked(true);
repo.statuses(Some(&mut opts))
.map(|statuses| {
statuses
.iter()
.filter(|e| {
let s = e.status();
s.is_wt_modified()
|| s.is_index_modified()
|| s.is_wt_deleted()
|| s.is_index_deleted()
|| s.is_conflicted()
|| s.is_wt_new()
|| s.is_index_new()
})
.count()
})
.unwrap_or(0)
}
pub fn get_remote_url(repo: &Repository) -> Option<String> {
let remote_name = get_remote_name(repo)?;
repo.find_remote(&remote_name)
.ok()
.and_then(|r| r.url().map(ToOwned::to_owned))
}
pub fn fetch_origin(repo: &Repository) -> anyhow::Result<()> {
let remote_name = get_remote_name(repo).ok_or_else(|| anyhow::anyhow!("No remotes found"))?;
let path = repo
.path()
.parent()
.ok_or_else(|| anyhow::anyhow!("No parent directory found"))?;
let output = Command::new("git")
.arg("fetch")
.arg(&remote_name)
.current_dir(path)
.output()?;
if !output.status.success() {
anyhow::bail!(
"Failed to fetch from {}: {}",
remote_name,
String::from_utf8_lossy(&output.stderr)
)
}
Ok(())
}
pub fn merge_ff(repo: &Repository) -> anyhow::Result<bool> {
let head = repo.head()?;
if head.is_branch() {
let branch = Branch::wrap(head);
let upstream = branch.upstream()?;
let upstream_head_commit = repo.reference_to_annotated_commit(upstream.get())?;
if let Ok((merge_analysis, merge_pref)) = repo.merge_analysis(&[&upstream_head_commit])
&& merge_analysis.is_fast_forward()
&& !merge_pref.is_no_fast_forward()
{
let upstream_head_commit_id = upstream_head_commit.id();
repo.checkout_tree(&repo.find_object(upstream_head_commit_id, None)?, None)?;
repo.head()?
.set_target(upstream_head_commit_id, "updated by git-statuses")?;
return Ok(true);
}
}
Ok(false)
}
pub fn get_branch_push_status(repo: &Repository) -> Status {
let Ok(head) = repo.head() else {
return Status::Unknown;
};
if !head.is_branch() {
return Status::Detached;
}
let Some(local_branch) = head.shorthand() else {
return Status::Unknown;
};
let Some(local_oid) = head.target() else {
return Status::Unknown;
};
let Some(remote_name) = get_remote_name(repo) else {
return Status::Unpublished;
};
let Ok(remote_ref) = repo.find_reference(&format!("refs/remotes/{remote_name}/{local_branch}"))
else {
return Status::Unpublished;
};
let Some(remote_oid) = remote_ref.target() else {
return Status::Unpublished;
};
match repo.graph_ahead_behind(local_oid, remote_oid) {
Ok((ahead, _)) if ahead > 0 => Status::Unpushed,
Ok(_) => Status::Clean,
Err(_) => Status::Unknown,
}
}
pub fn get_stash_count(repo: &mut Repository) -> usize {
let mut count = 0;
let _ = repo.stash_foreach(|_, _, _| {
count += 1;
true });
count
}