Replace Git subprocesses with native APIs

This commit is contained in:
Georg Bauer
2026-07-28 06:55:05 +02:00
parent 90a445cafd
commit ed2226bc60
5 changed files with 457 additions and 254 deletions

View File

@@ -368,48 +368,31 @@ impl App {
}
fn read_git_state(path: &Path) -> Option<GitState> {
let path = path.to_str()?;
let branches = std::process::Command::new("git")
.args([
"-C",
path,
"for-each-ref",
"--format=%(refname:short)",
"refs/heads",
])
.output()
let repository = gix::discover(path).ok()?;
let mut branches = repository
.references()
.ok()?
.local_branches()
.ok()?
.map(|reference| {
reference
.map(|reference| String::from_utf8_lossy(reference.name().shorten()).into_owned())
})
.collect::<Result<Vec<_>, _>>()
.ok()?;
if !branches.status.success() {
return None;
}
let mut branches = String::from_utf8_lossy(&branches.stdout)
.lines()
.map(str::trim)
.filter(|branch| !branch.is_empty())
.map(str::to_owned)
.collect::<Vec<_>>();
branches.sort();
branches.dedup();
let current = std::process::Command::new("git")
.args(["-C", path, "symbolic-ref", "--quiet", "--short", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
.filter(|branch| !branch.is_empty());
let current = repository
.head_name()
.ok()?
.map(|name| String::from_utf8_lossy(name.shorten()).into_owned());
let label = current.clone().unwrap_or_else(|| {
std::process::Command::new("git")
.args(["-C", path, "rev-parse", "--short", "HEAD"])
.output()
repository
.head_id()
.ok()
.filter(|output| output.status.success())
.map(|output| {
format!(
"detached @ {}",
String::from_utf8_lossy(&output.stdout).trim()
)
})
.and_then(|id| id.shorten().ok())
.map(|id| format!("detached @ {id}"))
.unwrap_or_else(|| "No branch".to_owned())
});
Some(GitState {
@@ -420,22 +403,25 @@ fn read_git_state(path: &Path) -> Option<GitState> {
}
fn git_switch(path: &Path, branch: &str) -> Result<(), String> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(path)
.args(["switch", "--", branch])
.output()
.map_err(|error| format!("Could not run Git: {error}"))?;
if output.status.success() {
Ok(())
} else {
let detail = String::from_utf8_lossy(&output.stderr).trim().to_owned();
Err(if detail.is_empty() {
"Could not switch Git branches.".into()
} else {
format!("Could not switch Git branches: {detail}")
})
let repository = git2::Repository::discover(path)
.map_err(|error| format!("Could not switch Git branches: {error}"))?;
let reference = format!("refs/heads/{branch}");
let previous = repository
.find_reference("HEAD")
.ok()
.map(|head| (head.symbolic_target().map(str::to_owned), head.target()));
repository
.set_head(&reference)
.map_err(|error| format!("Could not switch Git branches: {error}"))?;
if let Err(error) = repository.checkout_head(Some(git2::build::CheckoutBuilder::new().safe())) {
if let Some((Some(reference), _)) = &previous {
let _ = repository.set_head(reference);
} else if let Some((_, Some(target))) = previous {
let _ = repository.set_head_detached(target);
}
return Err(format!("Could not switch Git branches: {error}"));
}
Ok(())
}
pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) -> String {
@@ -450,7 +436,6 @@ pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) ->
mod tests {
use super::*;
use crate::database::{Project, Session};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
@@ -541,37 +526,30 @@ mod tests {
std::process::id()
));
std::fs::create_dir(&directory).unwrap();
for arguments in [
&["init", "-b", "main"][..],
&["config", "user.name", "DS4Server Test"],
&["config", "user.email", "test@ds4server.invalid"],
] {
assert!(
Command::new("git")
.arg("-C")
.arg(&directory)
.args(arguments)
.status()
.unwrap()
.success()
);
}
let mut options = git2::RepositoryInitOptions::new();
options.initial_head("main");
let repository = git2::Repository::init_opts(&directory, &options).unwrap();
let mut config = repository.config().unwrap();
config.set_str("user.name", "DS4Server Test").unwrap();
config
.set_str("user.email", "test@ds4server.invalid")
.unwrap();
drop(config);
std::fs::write(directory.join("tracked.txt"), "test").unwrap();
for arguments in [
&["add", "tracked.txt"][..],
&["commit", "-m", "Initial"],
&["branch", "feature"],
] {
assert!(
Command::new("git")
.arg("-C")
.arg(&directory)
.args(arguments)
.status()
.unwrap()
.success()
);
}
let mut index = repository.index().unwrap();
index.add_path(Path::new("tracked.txt")).unwrap();
let tree_id = index.write_tree().unwrap();
index.write().unwrap();
let tree = repository.find_tree(tree_id).unwrap();
let signature = repository.signature().unwrap();
let commit = repository
.commit(Some("HEAD"), &signature, &signature, "Initial", &tree, &[])
.unwrap();
let commit = repository.find_commit(commit).unwrap();
repository.branch("feature", &commit, false).unwrap();
drop(commit);
drop(tree);
drop(repository);
let state = read_git_state(&directory).unwrap();
assert_eq!(state.current.as_deref(), Some("main"));