KeiSeiKit-1.0/_primitives/_rust/kei-fork/src/git.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

198 lines
6.5 KiB
Rust

//! Thin `Command::new(git_bin())` wrappers.
//!
//! Every helper runs `git` in `kit_root` (or a specified worktree),
//! captures stdout/stderr, and returns `Error::Git` on non-zero exit.
//! No parsing beyond `trim()` on stdout — callers interpret the string.
//!
//! PATH hijack mitigation (HIGH #4): the git binary is resolved via
//! `git_bin()`, which honours `KEI_FORK_GIT_BIN` if set. Ops can pin to
//! an absolute path (e.g. `/usr/bin/git`) in trusted environments.
//!
//! Arg-injection mitigation (HIGH #3): `worktree_add` uses the `--`
//! sentinel before the base commit-ish and validates the refname shape.
use crate::error::Error;
use std::ffi::OsString;
use std::path::Path;
use std::process::{Command, Output};
/// Resolve the `git` binary. Honours `KEI_FORK_GIT_BIN` for hardening.
pub fn git_bin() -> OsString {
std::env::var_os("KEI_FORK_GIT_BIN").unwrap_or_else(|| OsString::from("git"))
}
fn run(cmd_desc: &str, c: &mut Command) -> Result<Output, Error> {
let out = c.output().map_err(Error::Io)?;
if !out.status.success() {
return Err(Error::Git {
cmd: cmd_desc.to_string(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
});
}
Ok(out)
}
/// Conservative git refname validator. Accepts the subset we emit and
/// the subset a caller may reasonably pass as `base`. Rejects leading
/// `-` (option injection), NUL, newline, and characters outside a
/// deliberately narrow allowlist.
pub fn is_safe_refname(s: &str) -> bool {
if s.is_empty() || s.len() > 255 {
return false;
}
let first = s.as_bytes()[0];
if first == b'-' || first == b'.' || first == b'/' {
return false;
}
for b in s.bytes() {
let ok = b.is_ascii_alphanumeric()
|| matches!(b, b'_' | b'-' | b'.' | b'/');
if !ok {
return false;
}
}
// No consecutive dots, no `..` traversal, no trailing `.lock`.
if s.contains("..") || s.ends_with(".lock") || s.ends_with('/') {
return false;
}
true
}
pub fn worktree_add(
kit_root: &Path,
worktree_rel: &Path,
new_branch: &str,
base: &str,
) -> Result<(), Error> {
if !is_safe_refname(new_branch) {
return Err(Error::InvalidRef(new_branch.to_string()));
}
if !is_safe_refname(base) {
return Err(Error::InvalidRef(base.to_string()));
}
let path_str = worktree_rel.to_str().ok_or_else(|| {
Error::Validate("worktree path is not valid UTF-8".to_string())
})?;
let mut c = Command::new(git_bin());
// `--` sentinel: everything after is positional. The branch name is
// pinned by `-b` (we already refname-validated it). The `base` is
// the commit-ish; placing it after `--` stops any accidental
// promotion to a flag if a future git version reorders parsing.
c.current_dir(kit_root).args([
"worktree",
"add",
"-b",
new_branch,
"--",
path_str,
base,
]);
run("git worktree add", &mut c)?;
Ok(())
}
/// Stage an explicit list of paths. Replacement for `add -A` which
/// bled unwanted markers (`.DONE`, meta files) into commits.
pub fn add_paths(cwd: &Path, paths: &[String]) -> Result<(), Error> {
if paths.is_empty() {
return Ok(());
}
let mut c = Command::new(git_bin());
c.current_dir(cwd).arg("add").arg("--");
for p in paths {
c.arg(p);
}
run("git add", &mut c)?;
Ok(())
}
/// List untracked files (respects `.gitignore`) relative to `cwd`.
pub fn ls_untracked(cwd: &Path) -> Result<Vec<String>, Error> {
let mut c = Command::new(git_bin());
c.current_dir(cwd)
.args(["ls-files", "-o", "--exclude-standard"]);
let out = run("git ls-files -o", &mut c)?;
Ok(split_lines(&out.stdout))
}
/// List modified tracked files relative to `cwd`.
pub fn ls_modified(cwd: &Path) -> Result<Vec<String>, Error> {
let mut c = Command::new(git_bin());
c.current_dir(cwd).args(["diff", "--name-only"]);
let out = run("git diff --name-only", &mut c)?;
Ok(split_lines(&out.stdout))
}
fn split_lines(stdout: &[u8]) -> Vec<String> {
String::from_utf8_lossy(stdout)
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_string())
.collect()
}
pub fn commit(cwd: &Path, msg: &str) -> Result<(), Error> {
let mut c = Command::new(git_bin());
c.current_dir(cwd).args(["commit", "--allow-empty", "-m", msg]);
run("git commit", &mut c)?;
Ok(())
}
pub fn rev_parse_head(cwd: &Path) -> Result<String, Error> {
let mut c = Command::new(git_bin());
c.current_dir(cwd).args(["rev-parse", "HEAD"]);
let out = run("git rev-parse HEAD", &mut c)?;
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
pub fn merge_no_ff(kit_root: &Path, branch: &str, msg: &str) -> Result<(), Error> {
if !is_safe_refname(branch) {
return Err(Error::InvalidRef(branch.to_string()));
}
let mut c = Command::new(git_bin());
c.current_dir(kit_root)
.args(["merge", "--no-ff", branch, "-m", msg]);
run("git merge --no-ff", &mut c)?;
Ok(())
}
pub fn worktree_prune(kit_root: &Path) -> Result<(), Error> {
let mut c = Command::new(git_bin());
c.current_dir(kit_root).args(["worktree", "prune"]);
run("git worktree prune", &mut c)?;
Ok(())
}
pub fn worktree_remove_force(kit_root: &Path, worktree_abs: &Path) -> Result<(), Error> {
let path_str = worktree_abs.to_str().ok_or_else(|| {
Error::Validate("worktree path is not valid UTF-8".to_string())
})?;
let mut c = Command::new(git_bin());
c.current_dir(kit_root)
.args(["worktree", "remove", "--force", "--", path_str]);
run("git worktree remove --force", &mut c)?;
Ok(())
}
pub fn branch_delete(kit_root: &Path, branch: &str) -> Result<(), Error> {
if !is_safe_refname(branch) {
return Err(Error::InvalidRef(branch.to_string()));
}
let mut c = Command::new(git_bin());
c.current_dir(kit_root).args(["branch", "-D", branch]);
run("git branch -D", &mut c)?;
Ok(())
}
/// Check whether `branch` exists. `git show-ref` exits 0 if the ref is
/// present, non-zero otherwise — we treat both as valid data, no error.
pub fn branch_exists(kit_root: &Path, branch: &str) -> bool {
if !is_safe_refname(branch) {
return false;
}
let full = format!("refs/heads/{branch}");
let mut c = Command::new(git_bin());
c.current_dir(kit_root)
.args(["show-ref", "--verify", "--quiet", &full]);
c.status().map(|s| s.success()).unwrap_or(false)
}