Skip to content
Clone
fad06fd
518 lines 13.6 KB Rust
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<u64>, args: Vec<String>) -> 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<u32>,
start_after: Option<&str>,
points_at: Option<&str>,
exclude: &[String],
peel: bool,
) -> Vec<String> {
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<Cmd, BabyError> {
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<Cmd, BabyError> {
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<Cmd, BabyError> {
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<Cmd, BabyError> {
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<Cmd, BabyError> {
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<Cmd, BabyError> {
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<Cmd, BabyError> {
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))
}