use gix for git status
This commit is contained in:
+153
-106
@@ -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(¤t) {
|
||||
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(¤t) {
|
||||
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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user