GitBaby/src/command/error.rs
zhenyi 680411c4fb chore: initialize gitbaby repository
gitbaby is an asynchronous Rust library wrapping git operations, built on
gix + tokio + async-trait.

Module layout:
- archive, blame, blob, branch, cleanup, commit, compare, config,
  conflict, diff, merge, refs, remote, setup, share, submodule, tags,
  tree (feature modules)
- command: command scheduler (cmd/env/pipe/context/error)
- repo: RepositoryFacade trait (consumers implement, exposing a
  gix::Repository)

Features:
- async API on tokio 1.53
- order-preserving Env (Vec<(String, String)>)
- hand-written BabyError / CmdError, not using thiserror derive
- integration tests covering cmd_run / context / env / pipe (env keys
  prefixed for isolation)

CI: none, standard cargo build / test / clippy / fmt only
2026-08-14 18:10:04 +08:00

206 lines
5.2 KiB
Rust

use std::fmt;
use std::path::PathBuf;
#[derive(Debug)]
pub enum CmdError {
Spawn {
prog: String,
source: std::io::Error,
},
Io {
path: PathBuf,
source: std::io::Error,
},
NonZeroExit {
prog: String,
args: Vec<String>,
code: i32,
stderr: String,
},
Timeout {
prog: String,
timeout_ms: u64,
},
InvalidArgs(String),
Cancelled,
Pipe(String),
PayloadTooLarge {
cap_bytes: u64,
actual_bytes: u64,
},
Custom(String),
}
impl fmt::Display for CmdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Spawn { prog, source } => {
write!(f, "failed to spawn `{}`: {}", prog, source)
}
Self::Io { path, source } => {
write!(f, "io error at `{}`: {}", path.display(), source)
}
Self::NonZeroExit {
prog,
args,
code,
stderr,
} => {
let argv_summary = redact_argv(prog, args);
write!(
f,
"`{} {}` exited with code {}: {}",
prog,
argv_summary,
code,
stderr.trim()
)
}
Self::Timeout { prog, timeout_ms } => {
write!(f, "`{}` timed out after {}ms", prog, timeout_ms)
}
Self::InvalidArgs(msg) => write!(f, "invalid args: {}", msg),
Self::Cancelled => write!(f, "command was cancelled"),
Self::Pipe(msg) => write!(f, "pipe error: {}", msg),
Self::PayloadTooLarge {
cap_bytes,
actual_bytes,
} => write!(
f,
"output exceeds cap: {} bytes > {} bytes cap",
actual_bytes, cap_bytes
),
Self::Custom(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for CmdError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Spawn { source, .. } | Self::Io { source, .. } => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for CmdError {
fn from(source: std::io::Error) -> Self {
Self::Io {
path: PathBuf::new(),
source,
}
}
}
pub type CmdResult<T> = std::result::Result<T, CmdError>;
const REDACTED: &str = "***REDACTED***";
fn redact_argv(prog: &str, args: &[String]) -> String {
let mut out = String::new();
out.push_str(prog);
let mut redact_next = false;
for (idx, arg) in args.iter().enumerate() {
out.push(' ');
if redact_next {
out.push_str(REDACTED);
redact_next = false;
continue;
}
if SENSITIVE_FLAGS
.iter()
.any(|flag| arg == *flag || arg.starts_with(flag))
{
let label = if arg.contains('=') {
arg.split_once('=').map(|(k, _)| k).unwrap_or(arg)
} else {
arg.as_str()
};
out.push_str(label);
out.push('=');
out.push_str(REDACTED);
continue;
}
if URL_VALUE_FLAGS
.iter()
.any(|flag| arg == *flag || arg.starts_with(flag))
{
let label = if arg.contains('=') {
arg.split_once('=').map(|(k, _)| k).unwrap_or(arg)
} else {
arg.as_str()
};
out.push_str(label);
if idx + 1 < args.len() && looks_like_url(&args[idx + 1]) {
out.push(' ');
out.push_str(REDACTED);
let _ = arg;
continue;
}
out.push_str(" <value>");
continue;
}
if looks_like_url(arg) {
out.push_str(REDACTED);
continue;
}
if idx == args.len() - 1 && redact_next_marker(prog, arg) {
redact_next = true;
}
out.push_str(arg);
}
out
}
fn looks_like_url(s: &str) -> bool {
if s.starts_with("http://")
|| s.starts_with("https://")
|| s.starts_with("ssh://")
|| s.starts_with("git://")
|| s.starts_with("file://")
{
true
} else {
s.contains("://") && s.contains('@')
}
}
fn redact_next_marker(prog: &str, arg: &str) -> bool {
if prog != "git" {
return false;
}
matches!(
arg,
"credential.helper"
| "credential.username"
| "credential.password"
| "http.extraheader"
| "core.sshcommand"
| "core.gitproxy"
| "url.<base>.insteadof"
| "include.path"
)
}
static SENSITIVE_FLAGS: &[&str] = &[
"--upload-pack=",
"--receive-pack=",
"-c",
"--config=",
"--config-env=",
"-S",
"--gpg-sign=",
"--key-id=",
"--ssh-command=",
"--askpass=",
];
static URL_VALUE_FLAGS: &[&str] = &[
"remote.add",
"remote set-url",
"remote prune",
"--exec=",
"--uploadarchive=",
];