use gix for git status

This commit is contained in:
2026-06-03 23:16:40 +02:00
parent fe0a21bdb9
commit a56a2108ac
3 changed files with 228 additions and 107 deletions
+153 -106
View File
@@ -983,128 +983,175 @@ struct GitStatus {
stash_count: usize,
}
/// Get git status for a directory.
/// Returns None if not in a git repository.
/// Visitor for counting working tree changes.
struct WorkingChangeVisitor<'a> {
count: &'a mut usize,
}
impl<'a> gix_status::index_as_worktree_with_renames::VisitEntry<'a> for WorkingChangeVisitor<'a> {
type ContentChange = <gix_status::index_as_worktree::traits::FastEq as gix_status::index_as_worktree::traits::CompareBlobs>::Output;
type SubmoduleStatus = gix::submodule::Status;
fn visit_entry(&mut self, entry: gix_status::index_as_worktree_with_renames::Entry<'a, Self::ContentChange, Self::SubmoduleStatus>) {
if let Some(summary) = entry.summary() {
match summary {
gix_status::index_as_worktree_with_renames::Summary::Added
| gix_status::index_as_worktree_with_renames::Summary::Modified
| gix_status::index_as_worktree_with_renames::Summary::Removed
| gix_status::index_as_worktree_with_renames::Summary::TypeChange
| gix_status::index_as_worktree_with_renames::Summary::Renamed
| gix_status::index_as_worktree_with_renames::Summary::Copied => {
*self.count += 1;
}
_ => {}
}
}
}
}
/// Get git status for a directory using gix (no subprocesses).
/// Returns None if not in a git repository or an error occurs.
fn get_git_status(cwd: &str) -> Option<GitStatus> {
use std::process::Command;
let repo = gix::discover(cwd).ok()?;
// Check if we're in a git repo and get the branch name
let head_output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(cwd)
.output()
.ok()?;
// Get branch name from HEAD
let head_name = repo.head_name().ok().flatten()?;
let head_name_bstr = head_name.as_bstr();
let head = head_name_bstr
.strip_prefix(b"refs/heads/")
.or(head_name_bstr.strip_prefix(b"refs/tags/"))
.or_else(|| head_name_bstr.strip_prefix(b"refs/"))
.map(|s| String::from_utf8_lossy(s).into_owned())
.unwrap_or_else(|| String::from_utf8_lossy(head_name_bstr).into_owned());
if !head_output.status.success() {
if head.is_empty() {
return None;
}
let head = String::from_utf8_lossy(&head_output.stdout)
.trim()
.to_string();
// Get ahead/behind against upstream
let mut ahead = 0usize;
let mut behind = 0usize;
if let Ok(head_id) = repo.head_id() {
if let Ok(head_ref) = repo.find_reference("HEAD") {
if let gix::refs::TargetRef::Symbolic(upstream_name) = head_ref.target() {
let upstream_full = format!("refs/remotes/origin/{}", upstream_name);
if let Ok(upstream_ref) = repo.find_reference(&upstream_full) {
let mut upstream_ref = upstream_ref;
if let Ok(upstream_id) = upstream_ref.peel_to_id() {
let head_id_detached = head_id.detach();
let upstream_id_detached = upstream_id.detach();
// Count ahead: commits reachable from head_id but not from upstream_id
let mut count = 0usize;
let mut seen = std::collections::HashSet::new();
let mut queue = vec![head_id_detached];
while let Some(current) = queue.pop() {
if seen.contains(&current) {
continue;
}
seen.insert(current);
if let Ok(commit) = repo.find_commit(current.clone()) {
for parent_oid in commit.parent_ids() {
let parent_oid_detached = parent_oid.detach();
if parent_oid_detached == upstream_id_detached {
break;
}
if !seen.contains(&parent_oid_detached) {
queue.push(parent_oid_detached);
}
}
}
count += 1;
}
ahead = count;
// Get ahead/behind status
let mut ahead = 0;
let mut behind = 0;
if let Ok(output) = Command::new("git")
.args(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"])
.current_dir(cwd)
.output()
{
if output.status.success() {
let counts = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = counts.trim().split_whitespace().collect();
if parts.len() == 2 {
ahead = parts[0].parse().unwrap_or(0);
behind = parts[1].parse().unwrap_or(0);
}
}
}
// Get working directory and staging status using git status --porcelain
let mut working_modified = 0;
let mut working_added = 0;
let mut working_deleted = 0;
let mut staging_modified = 0;
let mut staging_added = 0;
let mut staging_deleted = 0;
if let Ok(output) = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(cwd)
.output()
{
if output.status.success() {
let status = String::from_utf8_lossy(&output.stdout);
for line in status.lines() {
if line.len() < 2 {
continue;
}
let chars: Vec<char> = line.chars().collect();
let staging_char = chars[0];
let working_char = chars[1];
// Staging status (first column)
match staging_char {
'M' => staging_modified += 1,
'A' => staging_added += 1,
'D' => staging_deleted += 1,
'R' => staging_modified += 1, // renamed
'C' => staging_added += 1, // copied
_ => {}
}
// Working directory status (second column)
match working_char {
'M' => working_modified += 1,
'D' => working_deleted += 1,
'?' => working_added += 1, // untracked
_ => {}
// Count behind: commits reachable from upstream_id but not from head_id
let mut count = 0usize;
let mut seen = std::collections::HashSet::new();
let mut queue = vec![upstream_id_detached];
while let Some(current) = queue.pop() {
if seen.contains(&current) {
continue;
}
seen.insert(current);
if let Ok(commit) = repo.find_commit(current.clone()) {
for parent_oid in commit.parent_ids() {
let parent_oid_detached = parent_oid.detach();
if parent_oid_detached == head_id_detached {
break;
}
if !seen.contains(&parent_oid_detached) {
queue.push(parent_oid_detached);
}
}
}
count += 1;
}
behind = count;
}
}
}
}
}
// Build status strings like oh-my-posh format
let working_changed = working_modified + working_added + working_deleted;
let mut working_parts = Vec::new();
if working_modified > 0 {
working_parts.push(format!("~{}", working_modified));
}
if working_added > 0 {
working_parts.push(format!("+{}", working_added));
}
if working_deleted > 0 {
working_parts.push(format!("-{}", working_deleted));
}
let working_string = working_parts.join(" ");
// Get stash count from refs
let stash_count = if repo.find_reference("refs/stash").is_ok() {
1
} else {
0
};
let staging_changed = staging_modified + staging_added + staging_deleted;
let mut staging_parts = Vec::new();
if staging_modified > 0 {
staging_parts.push(format!("~{}", staging_modified));
}
if staging_added > 0 {
staging_parts.push(format!("+{}", staging_added));
}
if staging_deleted > 0 {
staging_parts.push(format!("-{}", staging_deleted));
}
let staging_string = staging_parts.join(" ");
// Get staging status (HEAD tree vs index) and working status (index vs worktree)
let mut staging_changed = 0usize;
let mut working_changed = 0usize;
// Get stash count
let mut stash_count = 0;
if let Ok(output) = Command::new("git")
.args(["stash", "list"])
.current_dir(cwd)
.output()
{
if output.status.success() {
let stash = String::from_utf8_lossy(&output.stdout);
stash_count = stash.lines().count();
// Get HEAD tree ID for tree-index diff
if let Ok(head_tree_id) = repo.head_tree_id() {
// Count staging changes (HEAD tree vs index)
if let Ok(index) = repo.index() {
let _ = repo.tree_index_status(
&head_tree_id,
&index,
None::<&mut gix::Pathspec<'_>>,
gix::status::tree_index::TrackRenames::AsConfigured,
|change: gix_diff::index::ChangeRef<'_, '_>, _tree_idx: &gix::index::State, _worktree_idx: &gix::index::State| {
match change {
gix_diff::index::ChangeRef::Addition { .. } => staging_changed += 1,
gix_diff::index::ChangeRef::Deletion { .. } => staging_changed += 1,
gix_diff::index::ChangeRef::Modification { .. } => staging_changed += 1,
gix_diff::index::ChangeRef::Rewrite { .. } => staging_changed += 1,
}
{ let cf: std::ops::ControlFlow<()> = std::ops::ControlFlow::Continue(()); Ok::<_, Box<dyn std::error::Error + Send + Sync>>(cf) }
},
).ok();
}
}
// Count working changes (index vs worktree)
if let Ok(index) = repo.index() {
let _ = repo.index_worktree_status(
&index,
Vec::<&str>::new(),
&mut WorkingChangeVisitor { count: &mut working_changed },
gix_status::index_as_worktree::traits::FastEq,
gix::status::index_worktree::BuiltinSubmoduleStatus::new(repo.clone().into_sync(), gix::status::Submodule::AsConfigured { check_dirty: false }).ok()?,
&mut gix::progress::Discard,
&std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
Default::default(),
).ok();
}
// Build status strings matching original format
let working_string = if working_changed > 0 {
format!("{}", working_changed)
} else {
String::new()
};
let staging_string = if staging_changed > 0 {
format!("{}", staging_changed)
} else {
String::new()
};
Some(GitStatus {
head,
ahead,
@@ -3453,7 +3500,7 @@ fn setup_config_watcher(
fn main() {
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info"),
env_logger::Env::default().default_filter_or("warn"),
)
.init();