use super::*;
pub(super) fn run_git(cwd: &Path, args: &[&str]) -> Option<String> {
let output = StdCommand::new("git")
.args(args)
.current_dir(cwd)
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(
String::from_utf8_lossy(&output.stdout)
.trim_end()
.to_string(),
)
}
pub(super) fn parse_remote_label(remote: &str) -> Option<String> {
let trimmed = remote.trim().trim_end_matches(".git");
if trimmed.is_empty() {
return None;
}
let normalized = trimmed.replace(':', "/");
let without_scheme = normalized
.split_once("://")
.map(|(_, rest)| rest.to_string())
.unwrap_or(normalized);
let parts: Vec<&str> = without_scheme
.split('/')
.filter(|part| !part.is_empty())
.collect();
if parts.len() < 2 {
return None;
}
Some(format!(
"{}/{}",
parts[parts.len() - 2],
parts[parts.len() - 1]
))
}
pub(super) fn parse_status_porcelain(
raw: &str,
) -> (GitStatusCounts, HashMap<String, ChangedFileStat>) {
let mut counts = GitStatusCounts::default();
let mut file_map = HashMap::new();
for line in raw.lines() {
if line.len() < 3 {
continue;
}
let status = &line[..2];
let path = line[3..].trim();
if path.is_empty() {
continue;
}
let normalized_status = if status == "??" {
counts.untracked += 1;
"?".to_string()
} else {
let x = status.chars().next().unwrap_or(' ');
let y = status.chars().nth(1).unwrap_or(' ');
if x != ' ' {
counts.staged += 1;
}
if status.contains('R') {
counts.renamed += 1;
"R".to_string()
} else if status.contains('A') {
counts.added += 1;
"A".to_string()
} else if status.contains('D') {
counts.deleted += 1;
"D".to_string()
} else {
if x != ' ' || y != ' ' {
counts.modified += 1;
}
"M".to_string()
}
};
file_map.insert(
path.to_string(),
ChangedFileStat {
status: normalized_status,
path: path.to_string(),
additions: None,
deletions: None,
},
);
}
(counts, file_map)
}
pub(super) fn parse_numstat_pairs(
raw: &str,
cached_raw: &str,
) -> (HashMap<String, (Option<u64>, Option<u64>)>, u64, u64) {
let mut map = HashMap::new();
let mut total_additions = 0u64;
let mut total_deletions = 0u64;
for source in [raw, cached_raw] {
for line in source.lines() {
let mut parts = line.splitn(3, '\t');
let additions_raw = parts.next();
let deletions_raw = parts.next();
let path_raw = parts.next();
let (Some(additions_raw), Some(deletions_raw), Some(path_raw)) =
(additions_raw, deletions_raw, path_raw)
else {
continue;
};
let additions = additions_raw.parse::<u64>().ok();
let deletions = deletions_raw.parse::<u64>().ok();
let path = path_raw.to_string();
if let Some(value) = additions {
total_additions = total_additions.saturating_add(value);
}
if let Some(value) = deletions {
total_deletions = total_deletions.saturating_add(value);
}
let entry = map.entry(path).or_insert((None, None));
if let Some(value) = additions {
entry.0 = Some(entry.0.unwrap_or(0u64).saturating_add(value));
}
if let Some(value) = deletions {
entry.1 = Some(entry.1.unwrap_or(0u64).saturating_add(value));
}
}
}
(map, total_additions, total_deletions)
}