use std::process::Command;
pub struct GitResult {
pub stdout: String,
pub stderr: String,
pub success: bool,
}
pub fn run(args: &[&str]) -> GitResult {
let output = Command::new("git")
.args(args)
.output()
.expect("Failed to execute git. Is git installed?");
GitResult {
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
success: output.status.success(),
}
}
pub fn current_branch() -> String {
let result = run(&["branch", "--show-current"]);
if result.success && !result.stdout.is_empty() {
result.stdout
} else {
"HEAD (detached)".to_string()
}
}
pub fn is_git_repo() -> bool {
run(&["rev-parse", "--is-inside-work-tree"]).success
}
pub struct FileStatus {
pub path: String,
pub staged: bool,
pub kind: FileChangeKind,
}
#[derive(Clone, Copy)]
pub enum FileChangeKind {
Modified,
Added,
Deleted,
Renamed,
Untracked,
}
impl FileChangeKind {
pub fn icon(&self) -> &str {
match self {
FileChangeKind::Modified => "~",
FileChangeKind::Added => "+",
FileChangeKind::Deleted => "-",
FileChangeKind::Renamed => ">",
FileChangeKind::Untracked => "?",
}
}
pub fn label(&self) -> &str {
match self {
FileChangeKind::Modified => "modified",
FileChangeKind::Added => "added",
FileChangeKind::Deleted => "deleted",
FileChangeKind::Renamed => "renamed",
FileChangeKind::Untracked => "untracked",
}
}
}
pub fn parse_status() -> Vec<FileStatus> {
let result = run(&["status", "--porcelain=v1"]);
if !result.success || result.stdout.is_empty() {
return vec![];
}
result
.stdout
.lines()
.filter_map(|line| {
if line.len() < 4 {
return None;
}
let index = line.as_bytes()[0];
let worktree = line.as_bytes()[1];
let path = line[3..].to_string();
if index != b' ' && index != b'?' {
let kind = match index {
b'M' => FileChangeKind::Modified,
b'A' => FileChangeKind::Added,
b'D' => FileChangeKind::Deleted,
b'R' => FileChangeKind::Renamed,
_ => FileChangeKind::Modified,
};
return Some(FileStatus {
path: path.clone(),
staged: true,
kind,
});
}
let kind = match worktree {
b'M' => FileChangeKind::Modified,
b'D' => FileChangeKind::Deleted,
b'?' => FileChangeKind::Untracked,
_ => return None,
};
Some(FileStatus {
path,
staged: false,
kind,
})
})
.collect()
}
pub fn ahead_behind() -> Option<(usize, usize)> {
let result = run(&["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
if !result.success {
return None;
}
let parts: Vec<&str> = result.stdout.split_whitespace().collect();
if parts.len() == 2 {
let ahead = parts[0].parse().unwrap_or(0);
let behind = parts[1].parse().unwrap_or(0);
Some((ahead, behind))
} else {
None
}
}
pub struct LogEntry {
#[allow(dead_code)]
pub hash: String,
pub subject: String,
pub relative_time: String,
pub is_merge: bool,
}
pub fn recent_log(count: usize) -> Vec<LogEntry> {
let n = format!("-{}", count);
let result = run(&["log", &n, "--pretty=format:%h|%s|%cr|%P"]);
if !result.success || result.stdout.is_empty() {
return vec![];
}
result
.stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(4, '|').collect();
if parts.len() < 4 {
return None;
}
let parents: Vec<&str> = parts[3].split_whitespace().collect();
Some(LogEntry {
hash: parts[0].to_string(),
subject: parts[1].to_string(),
relative_time: parts[2].to_string(),
is_merge: parents.len() > 1,
})
})
.collect()
}
pub fn branch_exists(name: &str) -> bool {
let result = run(&["rev-parse", "--verify", name]);
result.success
}
pub struct GraphCommit {
pub hash: String,
pub short_hash: String,
pub subject: String,
pub parents: Vec<String>,
}
pub fn commit_graph(max_count: usize) -> Vec<GraphCommit> {
let n = format!("-{}", max_count);
let result = run(&["log", "--all", &n, "--pretty=format:%H|%h|%s|%P"]);
if !result.success || result.stdout.is_empty() {
return vec![];
}
result
.stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(4, '|').collect();
if parts.len() < 4 {
return None;
}
let parents: Vec<String> = parts[3]
.split_whitespace()
.map(|s| s.to_string())
.collect();
Some(GraphCommit {
hash: parts[0].to_string(),
short_hash: parts[1].to_string(),
subject: parts[2].to_string(),
parents,
})
})
.collect()
}
pub fn branch_tips() -> Vec<(String, String)> {
let result = run(&[
"for-each-ref",
"--format=%(objectname) %(refname:short)",
"refs/heads/",
]);
if !result.success || result.stdout.is_empty() {
return vec![];
}
result
.stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(2, ' ').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect()
}
pub fn head_hash() -> Option<String> {
let result = run(&["rev-parse", "HEAD"]);
if result.success && !result.stdout.is_empty() {
Some(result.stdout)
} else {
None
}
}
pub fn last_action_type() -> Option<String> {
let result = run(&["reflog", "-1", "--pretty=format:%gs"]);
if result.success && !result.stdout.is_empty() {
Some(result.stdout)
} else {
None
}
}
pub fn last_commit_message() -> Option<String> {
let result = run(&["log", "-1", "--pretty=format:%s"]);
if result.success && !result.stdout.is_empty() {
Some(result.stdout)
} else {
None
}
}
pub fn reflog_entries(count: usize) -> Vec<(String, String, String)> {
let n = format!("-{}", count);
let result = run(&["reflog", &n, "--pretty=format:%h|%gs|%cr"]);
if !result.success || result.stdout.is_empty() {
return vec![];
}
result
.stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(3, '|').collect();
if parts.len() < 3 {
return None;
}
Some((
parts[0].to_string(),
parts[1].to_string(),
parts[2].to_string(),
))
})
.collect()
}