use super::atomic::write_unlinked;
use super::cmd::git_command;
use super::exported_refs;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use anyhow::Context as _;
use crate::config::ValidatedConfig;
use crate::snapshot::RepoSnapshot;
pub(crate) fn update_refs(export_dir: &Path, snapshot: &RepoSnapshot) -> anyhow::Result<()> {
let _ = std::fs::remove_dir_all(export_dir.join("refs"));
for sub in ["refs/heads", "refs/tags"] {
std::fs::create_dir_all(export_dir.join(sub))?;
}
let mut entries: Vec<(&str, &str, Option<&str>)> = snapshot
.public_refs
.iter()
.map(|r| (r.name.as_str(), r.raw.as_str(), r.peeled.as_deref()))
.chain(
snapshot
.cob_refs
.iter()
.map(|(name, raw)| (name.as_str(), raw.as_str(), None)),
)
.collect();
entries.sort_by_key(|&(name, _, _)| name);
let mut out = String::from("# pack-refs with: peeled fully-peeled sorted \n");
for (name, raw, peeled) in entries {
out.push_str(raw);
out.push(' ');
out.push_str(name);
out.push('\n');
if let Some(peeled) = peeled {
out.push('^');
out.push_str(peeled);
out.push('\n');
}
}
write_unlinked(&export_dir.join("packed-refs"), out.as_bytes())
}
pub(crate) fn source_path(
config: &ValidatedConfig,
snapshot: &RepoSnapshot,
) -> anyhow::Result<std::path::PathBuf> {
let layout = config.layout().context("open layout for repo export")?;
let source_path = layout
.guarded_path(&snapshot.repo_did)
.context("source repo path")?;
if !source_path.exists() {
anyhow::bail!("source repository not found at {}", source_path.display());
}
source_path
.canonicalize()
.with_context(|| format!("canonicalize source repo path {}", source_path.display()))
}
pub(crate) fn init_bare_export(
export_dir: &Path,
format: knot_types::ObjectFormat,
) -> anyhow::Result<()> {
for sub in [
"objects/info",
"objects/pack",
"refs/heads",
"refs/tags",
"info",
] {
std::fs::create_dir_all(export_dir.join(sub))?;
}
write_unlinked(&export_dir.join("HEAD"), b"ref: refs/heads/main\n")?;
let config = if format.capability() == "sha256" {
"[core]\n\trepositoryformatversion = 1\n\tfilemode = true\n\tbare = true\n[extensions]\n\tobjectformat = sha256\n"
} else {
"[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n"
};
write_unlinked(&export_dir.join("config"), config.as_bytes())?;
Ok(())
}
pub(crate) fn reset_export_dir(
export_dir: &Path,
format: knot_types::ObjectFormat,
) -> anyhow::Result<()> {
std::fs::remove_dir_all(export_dir).ok();
std::fs::create_dir_all(export_dir)?;
init_bare_export(export_dir, format)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportHead {
pub symbolic: Option<String>,
pub oid: Option<String>,
}
pub(crate) fn export_head_state(
export_dir: &Path,
snapshot: &RepoSnapshot,
) -> anyhow::Result<ExportHead> {
let raw = std::fs::read_to_string(export_dir.join("HEAD")).context("read export HEAD")?;
let trimmed = raw.trim();
if let Some(sym) = trimmed.strip_prefix("ref: ") {
let sym = sym.trim().to_string();
let oid = exported_refs(snapshot)
.find(|&(name, _)| name == sym)
.map(|(_, oid)| oid.to_string());
Ok(ExportHead {
symbolic: Some(sym),
oid,
})
} else if !trimmed.is_empty() {
Ok(ExportHead {
symbolic: None,
oid: Some(trimmed.to_string()),
})
} else {
Ok(ExportHead {
symbolic: None,
oid: None,
})
}
}
pub(crate) fn head_tip(export_dir: &Path) -> Option<String> {
let raw = std::fs::read_to_string(export_dir.join("HEAD")).ok()?;
let trimmed = raw.trim();
let Some(sym) = trimmed.strip_prefix("ref: ") else {
return (!trimmed.is_empty()).then(|| trimmed.to_string());
};
let sym = sym.trim();
if let Ok(loose) = std::fs::read_to_string(export_dir.join(sym)) {
let oid = loose.trim();
if !oid.is_empty() {
return Some(oid.to_string());
}
}
let packed = std::fs::read_to_string(export_dir.join("packed-refs")).ok()?;
for line in packed.lines() {
if let Some((oid, name)) = line.split_once(' ')
&& name == sym
{
return Some(oid.to_string());
}
}
None
}
pub(crate) fn set_export_head(
config: &ValidatedConfig,
snapshot: &RepoSnapshot,
export_dir: &Path,
) -> anyhow::Result<()> {
let default_ref = format!("refs/heads/{}", config.default_branch.as_str());
let target_ref = snapshot
.head_capture
.resolved
.as_deref()
.and_then(|head_oid| {
if snapshot
.public_refs
.iter()
.any(|r| r.name == default_ref && r.raw == head_oid)
{
Some(default_ref.clone())
} else if let Some(branch_ref) = snapshot
.public_refs
.iter()
.find(|r| r.name.starts_with("refs/heads/") && r.raw == head_oid)
.map(|r| r.name.clone())
{
Some(branch_ref)
} else {
let mut cmd = git_command(export_dir);
cmd.args(["branch", "--contains", head_oid]);
let output = cmd.output().ok()?;
output.status.success().then_some(())?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut branches = stdout
.lines()
.map(|l| l.trim().trim_start_matches('*').trim())
.filter(|l| !l.is_empty());
if branches
.clone()
.any(|b| b == config.default_branch.as_str())
{
Some(default_ref.clone())
} else {
branches.next().map(|first| format!("refs/heads/{first}"))
}
}
});
// with no public refs yet, HEAD keeps the branch the first push will create.
let unborn = || {
(snapshot.head_capture.symbolic.clone())
.filter(|sym| snapshot.public_refs.is_empty() && sym.starts_with("refs/heads/"))
};
let chosen_ref = target_ref
.or_else(|| {
snapshot
.public_refs
.iter()
.any(|r| r.name == default_ref)
.then_some(default_ref)
})
.or_else(unborn);
if let Some(target) = chosen_ref {
write_unlinked(
&export_dir.join("HEAD"),
format!("ref: {target}\n").as_bytes(),
)?;
}
Ok(())
}
pub(crate) fn is_reserved_ref(raw: &str) -> bool {
!knot_git::RefClass::of_revspec(raw).is_public() && !crate::snapshot::is_cob_ref(raw)
}
pub(crate) fn assert_export_refs(snapshot: &RepoSnapshot, export_dir: &Path) -> anyhow::Result<()> {
let repo = knot_git::Repo::open(export_dir).context("open export for ref check")?;
let expected: HashMap<&str, &str> = exported_refs(snapshot).collect();
let mut found_refs = HashSet::with_capacity(expected.len());
for record in repo.references().context("list export refs")? {
let name = record.name.as_str();
let oid = record.target.to_hex();
anyhow::ensure!(
!is_reserved_ref(name),
"export contains reserved ref: {name}"
);
let Some(&expected_oid) = expected.get(name) else {
anyhow::bail!("export contains unexpected ref not in snapshot: {name}");
};
anyhow::ensure!(
oid == expected_oid,
"export ref {name} points to {oid}, expected {expected_oid}"
);
found_refs.insert(name.to_string());
}
if let Some((expected, _)) =
exported_refs(snapshot).find(|(name, _)| !found_refs.contains(*name))
{
anyhow::bail!("export missing expected ref: {expected}");
}
Ok(())
}