use anyhow::{bail, Result};
use crate::commands::{materialize, serialize};
use crate::context::CommandContext;
fn expand_url(url: &str) -> String {
if url.contains(':') || url.starts_with('/') || url.starts_with('.') {
return url.to_string();
}
if url.matches('/').count() == 1 {
let url = url.strip_suffix(".git").unwrap_or(url);
return format!("git@github.com:{url}.git");
}
url.to_string()
}
fn check_remote_refs(
session: &git_meta_lib::Session,
url: &str,
ns: &str,
) -> Result<(bool, Vec<String>)> {
let output = git_meta_lib::git_utils::run_git(session.repo(), &["ls-remote", url])?;
let expected_ref = format!("refs/{ns}/main");
let mut has_match = false;
let mut other_namespaces = Vec::new();
for line in output.lines() {
let refname = match line.split('\t').nth(1) {
Some(r) => r.trim(),
None => continue,
};
if refname == expected_ref {
has_match = true;
} else if let Some(rest) = refname.strip_prefix("refs/") {
if let Some(candidate_ns) = rest.strip_suffix("/main") {
if !matches!(
candidate_ns,
"heads" | "tags" | "remotes" | "notes" | "stash"
) && !candidate_ns.contains('/')
{
other_namespaces.push(candidate_ns.to_string());
}
}
}
}
Ok((has_match, other_namespaces))
}
pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Result<()> {
let ctx = CommandContext::open(None)?;
let repo = ctx.session.repo();
let ns = namespace_override
.unwrap_or(ctx.session.namespace())
.to_string();
let url = expand_url(url);
let config = repo.config_snapshot();
let remote_url_key = format!("remote.{name}.url");
if config.string(&remote_url_key).is_some() {
bail!("remote '{name}' already exists");
}
eprintln!("Checking {url}...");
match check_remote_refs(&ctx.session, &url, &ns) {
Ok((has_match, other_namespaces)) => {
if !has_match {
if other_namespaces.is_empty() {
bail!(
"no metadata refs found on {url}\n\n\
The remote does not have refs/{ns}/main or any other recognizable metadata refs.\n\
If this is a new remote that will receive metadata via push, use:\n \
git meta remote add {url} --name {name} --namespace {ns}",
);
} else {
let found_refs = other_namespaces
.iter()
.map(|alt| format!(" refs/{alt}/main"))
.collect::<Vec<_>>()
.join("\n");
let suggestions = other_namespaces
.iter()
.map(|alt| format!(" git meta remote add {url} --namespace={alt}"))
.collect::<Vec<_>>()
.join("\n");
bail!(
"no metadata refs found under refs/{ns}/main on {url}\n\n\
However, metadata refs were found under other namespaces:\n{found_refs}\n\n\
To use one of these, re-run with --namespace:\n{suggestions}",
);
}
}
}
Err(e) => {
eprintln!("Warning: could not inspect remote refs: {e}");
eprintln!("Proceeding with setup anyway...");
}
}
let git_dir = repo.path();
let git_dir_str = git_dir.to_string_lossy();
let run = |args: &[&str]| -> Result<()> {
let mut full_args = vec!["--git-dir", &git_dir_str, "config"];
full_args.extend_from_slice(args);
let output = std::process::Command::new("git")
.args(&full_args)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git config failed: {}", stderr.trim());
}
Ok(())
};
let prefix = format!("remote.{name}");
run(&[&format!("{prefix}.url"), &url])?;
run(&[
&format!("{prefix}.fetch"),
&format!("+refs/{ns}/main:refs/{ns}/remotes/main"),
])?;
run(&[&format!("{prefix}.meta"), "true"])?;
run(&[&format!("{prefix}.promisor"), "true"])?;
run(&[&format!("{prefix}.partialclonefilter"), "blob:none"])?;
if namespace_override.is_some() {
run(&[&format!("{prefix}.metanamespace"), &ns])?;
}
println!("Added meta remote '{name}' -> {url}");
let fetch_refspec = format!("refs/{ns}/main:refs/{ns}/remotes/main");
eprint!("Fetching metadata (blobless)...");
match git_meta_lib::git_utils::run_git(
repo,
&["fetch", "--filter=blob:none", name, &fetch_refspec],
) {
Ok(_) => {
eprintln!(" done.");
let remote_ref = format!("{ns}/remotes/main");
let tracking_ref_name = format!("refs/{remote_ref}");
match repo.find_reference(&tracking_ref_name) {
Ok(r) => {
let tip_oid = r.into_fully_peeled_id()?.detach();
eprintln!(
" tracking ref: {} -> {}",
tracking_ref_name,
&tip_oid.to_string()[..12]
);
}
Err(e) => {
eprintln!(
" warning: tracking ref {tracking_ref_name} not found after fetch: {e}"
);
eprintln!("You can try again with: git meta pull");
return Ok(());
}
}
eprint!("Hydrating tip blobs...");
let blob_count =
git_meta_lib::git_utils::hydrate_tip_blobs_counted(repo, name, &remote_ref)?;
eprintln!(" {blob_count} blobs fetched.");
eprint!("Serializing local metadata...");
serialize::run(false)?;
eprintln!(" done.");
eprint!("Materializing remote metadata...");
materialize::run(None, false, false)?;
eprintln!(" done.");
let tracking_ref_name = format!("refs/{ns}/remotes/main");
if let Ok(r) = repo.find_reference(&tracking_ref_name) {
if let Ok(tip_id) = r.into_fully_peeled_id() {
let count = git_meta_lib::sync::insert_promisor_entries(
repo,
ctx.session.store(),
tip_id.detach(),
None,
)?;
if count > 0 {
eprintln!("Indexed {count} keys from history (available on demand).");
}
}
}
}
Err(e) => {
eprintln!("\nWarning: initial fetch failed: {e}");
eprintln!("You can fetch later with: git meta pull");
}
}
Ok(())
}
pub fn run_remove(name: &str) -> Result<()> {
let ctx = CommandContext::open(None)?;
let repo = ctx.session.repo();
let ns = ctx.session.namespace();
let config = repo.config_snapshot();
let meta_key = format!("remote.{name}.meta");
let is_meta = config.boolean(&meta_key).unwrap_or(false);
if !is_meta {
bail!("'{name}' is not a metadata remote (no meta = true)");
}
let git_dir = repo.path();
let git_dir_str = git_dir.to_string_lossy();
let unset = |key: &str| {
let _ = std::process::Command::new("git")
.args(["--git-dir", &git_dir_str, "config", "--unset-all", key])
.output();
};
unset(&format!("remote.{name}.url"));
unset(&format!("remote.{name}.fetch"));
unset(&format!("remote.{name}.meta"));
unset(&format!("remote.{name}.promisor"));
unset(&format!("remote.{name}.partialclonefilter"));
unset(&format!("remote.{name}.metanamespace"));
let ref_prefix = format!("refs/{ns}/remotes/");
let mut refs_to_delete = Vec::new();
let platform = repo.references()?;
for reference in platform.all()? {
let reference = reference.map_err(|e| anyhow::anyhow!("{e}"))?;
let name_str = reference.name().as_bstr().to_string();
if name_str.starts_with(&ref_prefix) {
refs_to_delete.push(name_str);
}
}
for refname in &refs_to_delete {
let reference = repo.find_reference(refname)?;
reference.delete().map_err(|e| anyhow::anyhow!("{e}"))?;
println!("Deleted ref {refname}");
}
let local_prefix = format!("refs/{ns}/local/");
let mut local_refs_to_delete = Vec::new();
let platform = repo.references()?;
for reference in platform.all()? {
let reference = reference.map_err(|e| anyhow::anyhow!("{e}"))?;
let name_str = reference.name().as_bstr().to_string();
if name_str.starts_with(&local_prefix) {
local_refs_to_delete.push(name_str);
}
}
for refname in &local_refs_to_delete {
let reference = repo.find_reference(refname)?;
reference.delete().map_err(|e| anyhow::anyhow!("{e}"))?;
println!("Deleted ref {refname}");
}
println!("Removed meta remote '{name}'");
Ok(())
}
pub fn run_list() -> Result<()> {
let ctx = CommandContext::open(None)?;
let remotes = git_meta_lib::git_utils::list_meta_remotes(ctx.session.repo())?;
if remotes.is_empty() {
println!("No metadata remotes configured.");
println!("Add one with: git meta remote add <url>");
} else {
for (name, url) in &remotes {
println!("{name}\t{url}");
}
}
Ok(())
}