use super::atomic::write_unlinked_with;
use super::refs::init_bare_export;
use std::path::Path;
use std::process::{Command, Output};
use anyhow::Context as _;
/// a temp file, not a pipe, because large input would deadlock against the child's output.
pub(crate) fn stdin_bytes(bytes: &[u8]) -> anyhow::Result<std::process::Stdio> {
use std::io::{Seek as _, Write as _};
let mut file = tempfile::tempfile().context("create stdin file")?;
file.write_all(bytes).context("write stdin file")?;
file.rewind().context("rewind stdin file")?;
Ok(file.into())
}
/// no thin packs and offset deltas, because the reply gets framed as it is.
pub(crate) fn pack_objects_command(export_dir: &Path, revs: &str) -> anyhow::Result<Command> {
let mut command = git_command(export_dir);
command.args([
"pack-objects",
"--revs",
"--stdout",
"-q",
"--delta-base-offset",
]);
command.stdin(stdin_bytes(revs.as_bytes())?);
Ok(command)
}
pub(crate) fn objects_before_alternates(
command: &mut Command,
objects: &Path,
export_dir: &Path,
) -> anyhow::Result<()> {
command.env("GIT_OBJECT_DIRECTORY", std::path::absolute(objects)?);
let alternate = std::path::absolute(export_dir.join("objects"))?;
command.env("GIT_ALTERNATE_OBJECT_DIRECTORIES", alternate);
Ok(())
}
pub(crate) struct GitInfo {
pub(crate) version: String,
pub(crate) agent: String,
}
pub(crate) fn git_info() -> anyhow::Result<&'static GitInfo> {
static INFO: std::sync::OnceLock<GitInfo> = std::sync::OnceLock::new();
if let Some(info) = INFO.get() {
return Ok(info);
}
let probe = tempfile::tempdir().context("create git probe dir")?;
init_bare_export(probe.path(), knot_types::ObjectFormat::SHA1)?;
let mut version_cmd = git_command(probe.path());
version_cmd.arg("--version");
let version = String::from_utf8(run_cmd(version_cmd, "git --version")?.stdout)
.context("git --version")?;
let adv = upload_pack(probe.path(), &["--advertise-refs"], None, true)?;
let text = String::from_utf8(adv).context("parse git v2 advertisement")?;
let agent = text
.lines()
.find_map(|line| line.get(4..)?.strip_prefix("agent="))
.context("missing agent in git v2 advertisement")?
.to_string();
let _ = INFO.set(GitInfo { version, agent });
Ok(INFO.get().expect("git info initialized"))
}
pub(crate) fn upload_pack(
export_dir: &Path,
args: &[&str],
request: Option<&[u8]>,
v2: bool,
) -> anyhow::Result<Vec<u8>> {
let command = upload_pack_command(export_dir, args, request, v2)?;
Ok(run_cmd(command, "git upload-pack")?.stdout)
}
pub(crate) fn upload_pack_into(
export_dir: &Path,
request: &[u8],
v2: bool,
path: &Path,
) -> anyhow::Result<u64> {
let command = upload_pack_command(export_dir, &[], Some(request), v2)?;
command_into(command, path)
}
pub(crate) fn command_into(mut command: Command, path: &Path) -> anyhow::Result<u64> {
write_unlinked_with(path, |file| {
command.stdout(file.try_clone()?);
run_cmd(command, "git upload-pack").map(drop)
})
}
pub(crate) fn upload_pack_command(
export_dir: &Path,
args: &[&str],
request: Option<&[u8]>,
v2: bool,
) -> anyhow::Result<Command> {
let mut command = git_command(export_dir);
// refs/cobs and refs/tngl only leave through v2 replies.
if !v2 {
command.args(["-c", "transfer.hideRefs=refs/cobs"]);
command.args(["-c", "transfer.hideRefs=refs/tngl"]);
}
command.args(["upload-pack", "--stateless-rpc"]).args(args);
command.arg(".");
if v2 {
command.env("GIT_PROTOCOL", "version=2");
}
if let Some(request) = request {
command.stdin(stdin_bytes(request)?);
}
Ok(command)
}
/// replace refs and grafts would make git walk a history no client gets.
pub(crate) fn true_history(command: &mut Command) -> &mut Command {
command
.env("GIT_NO_REPLACE_OBJECTS", "1")
// /dev/null/grafts can't exist, plain /dev/null makes git print a grafts deprecation hint.
.env("GIT_GRAFT_FILE", "/dev/null/grafts")
}
pub(crate) fn git_command(cwd: &Path) -> Command {
let mut command = Command::new("git");
true_history(&mut command)
.current_dir(cwd)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_COUNT", "3")
.env("GIT_CONFIG_KEY_0", "maintenance.auto")
.env("GIT_CONFIG_VALUE_0", "false")
.env("GIT_CONFIG_KEY_1", "gc.autoDetach")
.env("GIT_CONFIG_VALUE_1", "false")
.env("GIT_CONFIG_KEY_2", "safe.directory")
.env("GIT_CONFIG_VALUE_2", "*")
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "true")
.env("LC_ALL", "C");
without_repo_env(&mut command);
command
}
pub(crate) const REPO_LOCATING_ENV: [&str; 7] = [
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_NAMESPACE",
"GIT_CEILING_DIRECTORIES",
];
pub(crate) fn without_repo_env(command: &mut Command) -> &mut Command {
for name in REPO_LOCATING_ENV {
command.env_remove(name);
}
command
}
pub(crate) fn run_cmd(mut cmd: Command, desc: &str) -> anyhow::Result<Output> {
let output = cmd
.output()
.with_context(|| format!("failed to spawn {desc}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
anyhow::bail!(
"{desc} failed (status {}): {stderr} {stdout}",
output.status
);
}
Ok(output)
}