use std::io; use std::path::Path; use crate::command::cmd::Cmd; use crate::command::env::Env; use crate::error::BabyError; pub fn git_cmd(caller: &str, dir: &Path, env: Env, timeout: Option, args: Vec) -> Cmd { Cmd::new( caller.to_string(), "git", args, Vec::new(), dir.to_path_buf(), env, timeout, ) } pub fn reject_starts_with_dash(value: &str, kind: &str) -> Result<(), BabyError> { if value.starts_with('-') { Err(BabyError::InvalidGitArg(format!( "{} must not start with '-': `{}`", kind, value ))) } else { Ok(()) } } pub fn validate_remote_url(url: &str) -> Result<(), BabyError> { reject_starts_with_dash(url, "remote url")?; let lower = url.to_ascii_lowercase(); if lower.starts_with("ext::") || lower.starts_with("ssh::") || lower.starts_with("git::") || lower.starts_with("remote-") { return Err(BabyError::InvalidRemoteUrl(url.to_string())); } let has_scheme = lower.starts_with("http://") || lower.starts_with("https://") || lower.starts_with("ssh://") || lower.starts_with("git://") || lower.starts_with("file://"); if !has_scheme { // Allow scp-style: [user@]host:path or relative/absolute local paths if url.contains("://") { return Err(BabyError::InvalidRemoteUrl(url.to_string())); } if url.starts_with('-') { return Err(BabyError::InvalidRemoteUrl(url.to_string())); } if url.contains('\0') || url.contains('\n') || url.contains('\r') { return Err(BabyError::InvalidRemoteUrl(url.to_string())); } } Ok(()) } pub fn validate_revision(rev: &str) -> Result<(), BabyError> { crate::share::path::validate_revision_non_empty(rev)?; reject_starts_with_dash(rev, "revision")?; if rev.contains('\0') || rev.contains('\n') || rev.contains('\r') { return Err(BabyError::RevisionInvalid(rev.to_string())); } Ok(()) } pub fn validate_config_key(key: &str) -> Result<(), BabyError> { reject_starts_with_dash(key, "config key")?; if key.is_empty() { return Err(BabyError::InvalidGitArg("empty config key".to_string())); } let mut chars = key.chars(); let first = chars.next().unwrap(); if !(first.is_ascii_alphabetic() || first == '_') { return Err(BabyError::InvalidGitArg(format!( "config key must start with letter or '_': `{}`", key ))); } for ch in chars { if !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '.' || ch == '_') { return Err(BabyError::InvalidGitArg(format!( "config key contains invalid character `{}` in `{}`", ch, key ))); } } if is_dangerous_config_key(key) { return Err(BabyError::InvalidGitArg(format!( "refusing to set dangerous config key `{}` via this API; use managed_config_* or explicit global opt-in", key ))); } Ok(()) } const DANGEROUS_CONFIG_KEY_PATTERNS: &[&str] = &[ "core.sshcommand", "core.sshcommand.", "core.hookspath", "core.hookspath.", "core.gitproxy", "core.gitproxy.", "credential.helper", "credential.helper.", "credential.username", "credential.password", "credential.token", "http.extraheader", "http.extraheader.", "url.", "include.path", "includeif.", "init.templatedir", "protocol.file.allow", "protocol.ext.allow", "diff.", "filter.", "gc.auto", "gc.autodetach", "gpg.format", "gpg.program", "ssh.variant", "alias.", "remote.", "branch.", "tag.", ]; pub fn is_dangerous_config_key(key: &str) -> bool { let lower = key.to_ascii_lowercase(); DANGEROUS_CONFIG_KEY_PATTERNS .iter() .any(|pat| lower == *pat || lower.starts_with(pat)) } pub fn classify_spawn_error(prog: String, source: io::Error) -> BabyError { BabyError::Spawn { prog, source, stderr: String::new(), } } pub fn for_each_ref_format(peel: bool) -> String { if peel { "%(objectname) %(objecttype) %(refname) %(*objectname)".to_string() } else { "%(objectname) %(objecttype) %(refname)".to_string() } } pub fn build_for_each_ref_args( patterns: &[String], sort: Option<&str>, limit: Option, start_after: Option<&str>, points_at: Option<&str>, exclude: &[String], peel: bool, ) -> Vec { let mut args = vec![ "for-each-ref".to_string(), format!("--format={}", for_each_ref_format(peel)), ]; if let Some(s) = sort { args.push("--sort".to_string()); args.push(s.to_string()); } if let Some(l) = limit { args.push("--count".to_string()); args.push(l.to_string()); } if let Some(sa) = start_after { args.push("--start-after".to_string()); args.push(sa.to_string()); } if let Some(oid) = points_at { args.push("--points-at".to_string()); args.push(oid.to_string()); } for p in patterns { args.push(p.clone()); } for p in exclude { args.push("--exclude".to_string()); args.push(p.clone()); } args } pub enum CatFileSubcommand { BatchCheck, Batch, Blob, } pub fn git_cat_file_cmd( dir: &Path, env: Env, oid: &gix::ObjectId, subcommand: CatFileSubcommand, ) -> Cmd { let mut args = vec!["cat-file".to_string()]; match subcommand { CatFileSubcommand::BatchCheck => { args.push("--batch-check".to_string()); } CatFileSubcommand::Batch => { args.push("--batch".to_string()); } CatFileSubcommand::Blob => { args.push("blob".to_string()); args.push("--end-of-options".to_string()); args.push(oid.to_string()); } } git_cmd("gitbaby::blob", dir, env, None, args) } pub fn git_hash_object_cmd(dir: &Path, env: Env, path: Option<&str>) -> Cmd { let mut args = vec![ "hash-object".to_string(), "--stdin".to_string(), "-w".to_string(), ]; if let Some(p) = path { args.push("--path".to_string()); args.push(p.to_string()); } git_cmd("gitbaby::blob", dir, env, None, args) } pub enum ConfigScope { Global, Local, } pub enum ConfigOp { Get, Set, Add, UnsetAll, GetAll, GetRegexp, } pub fn build_config_cmd( caller: &str, dir: &Path, env: Env, scope: ConfigScope, op: ConfigOp, key: &str, value: Option<&str>, ) -> Result { validate_config_key(key)?; if let Some(v) = value { reject_starts_with_dash(v, "config value")?; if v.contains('\0') { return Err(BabyError::InvalidGitArg(format!( "config value contains NUL: `{}`", v ))); } } let mut args = vec!["config".to_string()]; if matches!(scope, ConfigScope::Global) { args.push("--global".to_string()); } match op { ConfigOp::Get => args.push("--get".to_string()), ConfigOp::Set => {} ConfigOp::Add => args.push("--add".to_string()), ConfigOp::UnsetAll => args.push("--unset-all".to_string()), ConfigOp::GetAll => args.push("--get-all".to_string()), ConfigOp::GetRegexp => args.push("--get-regexp".to_string()), } args.push(key.to_string()); if let Some(v) = value && matches!(op, ConfigOp::Set | ConfigOp::Add) { args.push(v.to_string()); } Ok(git_cmd(caller, dir, env, None, args)) } pub enum ArchiveFormatArg { Tar, TarGz, TarBz2, Zip, } pub fn build_archive_cmd( caller: &str, dir: &Path, env: Env, format: ArchiveFormatArg, prefix: Option<&str>, commit: &str, paths: &[String], ) -> Result { validate_revision(commit)?; if let Some(p) = prefix { reject_starts_with_dash(p, "archive prefix")?; if p.contains('\0') || p.contains('\n') || p.contains('\r') { return Err(BabyError::InvalidGitArg(format!( "archive prefix contains control character: `{}`", p ))); } } for p in paths { reject_starts_with_dash(p, "archive path")?; } let mut args = vec!["archive".to_string()]; match format { ArchiveFormatArg::Tar => args.push("--format=tar".to_string()), ArchiveFormatArg::TarGz => args.push("--format=tar.gz".to_string()), ArchiveFormatArg::TarBz2 => args.push("--format=tar.bz2".to_string()), ArchiveFormatArg::Zip => args.push("--format=zip".to_string()), } if let Some(p) = prefix { args.push(format!("--prefix={}", p)); } args.push("--end-of-options".to_string()); args.push(commit.to_string()); if !paths.is_empty() { args.push("--".to_string()); for p in paths { args.push(p.clone()); } } Ok(git_cmd(caller, dir, env, None, args)) } pub enum RemoteOp { Add, Remove, GetUrl, Show, } pub enum RemoteMirror { Default, Push, Fetch, } pub fn build_remote_op_cmd( caller: &str, dir: &std::path::Path, env: Env, op: RemoteOp, name: &str, url: Option<&str>, mirror: RemoteMirror, ) -> Result { reject_starts_with_dash(name, "remote name")?; if name.is_empty() { return Err(BabyError::InvalidGitArg("remote name is empty".to_string())); } if let Some(u) = url { validate_remote_url(u)?; } let mut args = vec!["remote".to_string()]; match op { RemoteOp::Add => args.push("add".to_string()), RemoteOp::Remove => args.push("rm".to_string()), RemoteOp::GetUrl => args.push("get-url".to_string()), RemoteOp::Show => args.push("show".to_string()), } match mirror { RemoteMirror::Default => {} RemoteMirror::Push => args.push("--mirror=push".to_string()), RemoteMirror::Fetch => args.push("--mirror=fetch".to_string()), } args.push("--end-of-options".to_string()); args.push(name.to_string()); if let Some(u) = url { args.push(u.to_string()); } Ok(git_cmd(caller, dir, env, None, args)) } pub fn build_ls_remote_cmd( caller: &str, dir: &std::path::Path, env: Env, url: &str, refs: &[String], ) -> Result { validate_remote_url(url)?; for r in refs { reject_starts_with_dash(r, "ref pattern")?; } let mut args = vec![ "ls-remote".to_string(), "--end-of-options".to_string(), url.to_string(), ]; for r in refs { args.push(r.clone()); } Ok(git_cmd(caller, dir, env, None, args)) } pub fn build_init_cmd( caller: &str, dir: &std::path::Path, env: Env, path: &str, bare: bool, object_format: Option<&str>, ) -> Result { if path.starts_with('-') { return Err(BabyError::InvalidGitArg(format!( "init path must not start with '-': `{}`", path ))); } if path.contains('\0') { return Err(BabyError::InvalidGitArg(format!( "init path contains NUL: `{}`", path ))); } if let Some(of) = object_format { reject_starts_with_dash(of, "object-format")?; } let mut args = vec!["init".to_string()]; if bare { args.push("--bare".to_string()); } if let Some(of) = object_format { args.push(format!("--object-format={}", of)); } args.push("--end-of-options".to_string()); args.push(path.to_string()); Ok(git_cmd(caller, dir, env, None, args)) } pub fn build_fast_import_cmd(caller: &str, dir: &std::path::Path, env: Env) -> Cmd { let args = vec![ "fast-import".to_string(), "--force".to_string(), "--done".to_string(), ]; git_cmd(caller, dir, env, None, args) } pub fn build_gc_cmd(caller: &str, dir: &std::path::Path, env: Env, prune_now: bool) -> Cmd { let mut args = vec!["gc".to_string()]; if prune_now { args.push("--prune=now".to_string()); } git_cmd(caller, dir, env, None, args) } pub fn build_pack_refs_cmd(caller: &str, dir: &std::path::Path, env: Env) -> Cmd { let args = vec!["pack-refs".to_string(), "--all".to_string()]; git_cmd(caller, dir, env, None, args) } pub fn build_fetch_cmd( caller: &str, dir: &std::path::Path, env: Env, remote_url: &str, refspecs: &[String], prune: bool, tags: bool, ) -> Result { validate_remote_url(remote_url)?; for r in refspecs { reject_starts_with_dash(r, "refspec")?; } let mut args = vec!["fetch".to_string()]; if prune { args.push("--prune".to_string()); } if !tags { args.push("--no-tags".to_string()); } args.push("--end-of-options".to_string()); args.push(remote_url.to_string()); for r in refspecs { args.push(r.clone()); } Ok(git_cmd(caller, dir, env, None, args)) } pub fn build_push_cmd( caller: &str, dir: &std::path::Path, env: Env, remote_url: &str, refspecs: &[String], force: bool, ) -> Result { validate_remote_url(remote_url)?; for r in refspecs { reject_starts_with_dash(r, "refspec")?; } let mut args = vec!["push".to_string()]; if force { args.push("--force".to_string()); } args.push("--end-of-options".to_string()); args.push(remote_url.to_string()); for r in refspecs { args.push(r.clone()); } Ok(git_cmd(caller, dir, env, None, args)) }