use crate::{
append_ref, config, crosshub, gitcmd, projection, refcode, repomap, repos, reposdiscover,
schema, store,
};
use crate::{short_id, valid_slug, PredicateFalse};
use anyhow::{anyhow, Result};
pub(crate) fn cmd_repos_map(slug: String, path: Option<String>) -> Result<()> {
if !valid_slug(&slug) {
return Err(anyhow!(
"invalid repo slug '{slug}': must match a repos/<slug> key ([a-z0-9][a-z0-9-]*)"
));
}
let dir = match path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
let abs = repomap::set(&slug, &dir)?;
println!("mapped {slug} → {}", abs.display());
if let Some(rsha) = crosshub::root_sha(&abs) {
println!(" root-sha {} (identity anchor)", &rsha[..rsha.len().min(12)]);
}
let known = config::repo_root().ok().map(|r| repos::load(&r).contains_key(&slug)).unwrap_or(false);
if !known {
eprintln!(
"note: '{slug}' isn't in this hub's repos/ registry — peers can't resolve `--ref {slug}:…` \
until it's shared (add repos/{slug}.md with its url + root_sha)."
);
}
Ok(())
}
pub(crate) fn cmd_repos_discover(roots: Vec<String>) -> Result<()> {
let roots: Vec<std::path::PathBuf> = roots.into_iter().map(std::path::PathBuf::from).collect();
let report = reposdiscover::run(&roots)?;
for (slug, path) in &report.mapped {
println!("mapped {slug} → {}", path.display());
}
for (slug, url) in &report.unmatched {
let url = url.as_deref().unwrap_or("(no url)");
println!("unmatched {slug} ({url}) — no local clone found");
}
if report.mapped.is_empty() && report.unmatched.is_empty() {
println!("no repos registered across the hubs you follow — nothing to discover.");
}
Ok(())
}
pub(crate) fn parse_ref_query(s: &str) -> Result<(String, Option<String>, Option<[u64; 2]>)> {
let bad = || anyhow!("invalid refs target '{s}': expected repo[:path[#Lstart-Lend]]");
let (repo, rest) = match s.split_once(':') {
Some((r, rest)) => (r.to_string(), Some(rest)),
None => (s.to_string(), None),
};
if repo.is_empty() {
return Err(bad());
}
let (path, range) = match rest {
None => (None, None),
Some(rest) => {
let (before_hash, range) = match rest.split_once('#') {
Some((p, span)) => (p, Some(append_ref::parse_range(span)?)),
None => (rest, None),
};
let path = before_hash.split('@').next().unwrap_or(before_hash);
(if path.is_empty() { None } else { Some(path.to_string()) }, range)
}
};
Ok((repo, path, range))
}
pub(crate) fn cmd_refs(target: String, check: bool, all_hubs: bool, json: bool) -> Result<()> {
let (repo, path, range) = parse_ref_query(&target)?;
let hubs: Vec<std::path::PathBuf> =
if all_hubs { crosshub::hub_dirs() } else { vec![config::repo_root()?] };
let mut hits: Vec<(String, projection::RefHit, &'static str)> = Vec::new();
for hub in &hubs {
let Ok(msgs) = store::all_messages(hub) else { continue };
let idx = projection::RefIndex::fold(&msgs);
let repo_inv = repos::load(hub);
let label = crosshub::hub_label(hub);
let mut clone_cache: std::collections::HashMap<String, Option<std::path::PathBuf>> =
std::collections::HashMap::new();
for h in idx.query(&repo, path.as_deref(), range) {
let clone = clone_cache
.entry(h.repo.clone())
.or_insert_with(|| refcode::clone_for(&repo_inv, &h.repo))
.clone();
let st = if h.kind == projection::RefKind::Patch {
refcode::patch_staleness(clone.as_deref(), &h.path, h.result_hash.as_deref()).label()
} else {
refcode::staleness_ex(
clone.as_deref(),
&h.sha,
&h.path,
h.content_hash.as_deref(),
h.base_ref.as_deref(),
h.fork_point.as_deref(),
)
.label()
};
hits.push((label.clone(), h.clone(), st));
}
}
if check {
return if hits.is_empty() { Err(PredicateFalse.into()) } else { Ok(()) };
}
if json {
for (hub, h, st) in &hits {
let mut refj = serde_json::json!({ "repo": h.repo, "path": h.path, "sha": h.sha });
if let Some(r) = h.range {
refj["range"] = serde_json::json!(r);
}
if let Some(ch) = &h.content_hash {
refj["content_hash"] = serde_json::json!(ch);
}
if let Some(n) = &h.ref_name {
refj["ref_name"] = serde_json::json!(n);
}
if let Some(t) = &h.ref_type {
refj["ref_type"] = serde_json::json!(t);
}
if let Some(d) = &h.commit_date {
refj["commit_date"] = serde_json::json!(d);
}
if h.dirty {
refj["dirty"] = serde_json::json!(true);
}
if h.untracked {
refj["untracked"] = serde_json::json!(true);
}
if let Some(b) = &h.base_ref {
refj["base_ref"] = serde_json::json!(b);
}
if let Some(f) = &h.fork_point {
refj["fork_point"] = serde_json::json!(f);
}
if h.kind == projection::RefKind::Patch {
refj["patch"] = serde_json::json!(true);
}
if let Some(rh) = &h.result_hash {
refj["result_hash"] = serde_json::json!(rh);
}
let line = serde_json::json!({
"event": "ref-hit",
"hub": hub,
"ref": refj,
"staleness": st,
"message": {
"id": h.msg_id, "from": h.from, "type": h.msg_type,
"ts": h.ts, "topic": h.topic, "summary": h.summary,
},
"thread": { "root": h.thread_root, "status": h.request_status },
});
println!("{}", serde_json::to_string(&line)?);
}
return Ok(());
}
let target_disp = match (&path, range) {
(Some(p), Some(r)) => format!("{repo}:{p}#L{}-{}", r[0], r[1]),
(Some(p), None) => format!("{repo}:{p}"),
(None, _) => repo.clone(),
};
if hits.is_empty() {
println!("no conversations reference {target_disp}");
return Ok(());
}
println!("{} conversation(s) reference {target_disp}:", hits.len());
for (hub, h, st) in &hits {
let hubp = if all_hubs { format!("{hub} · ") } else { String::new() };
let loc = h.topic.as_deref().map(|t| format!("#{t}")).unwrap_or_else(|| "—".into());
let status = h.request_status.map(|s| format!(" [{s}]")).unwrap_or_default();
let rng = h.range.map(|r| format!("#L{}-{}", r[0], r[1])).unwrap_or_default();
let paren = refcode::identity_paren(h.ref_name.as_deref(), h.ref_type.as_deref(), h.commit_date.as_deref());
let stmark = match *st {
"changed" => " ⚠changed",
"moved" => " ⚠moved",
"reachable" => " ⚠reachable",
"offline" => " ⚠offline",
"squashed" => " ⚠squashed",
"unpinned" => " ⚠unpinned — legacy",
_ => "",
};
let flags = match (h.dirty, h.untracked) {
(true, true) => " [dirty][untracked]",
(true, false) => " [dirty]",
(false, true) => " [untracked]",
(false, false) => "",
};
let patch_chip = if h.kind == projection::RefKind::Patch {
format!(" ⟳ proposed a change here ({})", if *st == "landed" { "applied" } else { "open" })
} else {
String::new()
};
println!(
" {hubp}{loc} {} {}{status} {} ({}:{}{paren}{rng}){stmark}{flags}{patch_chip}",
short_id(&h.msg_id),
h.from,
h.summary,
h.repo,
h.path
);
}
Ok(())
}
pub(crate) fn cmd_ref_contains(sha: String, against: String, repo: Option<String>) -> Result<()> {
let dir = match repo {
Some(slug) => {
let hub = config::repo_root()?;
let repo_inv = repos::load(&hub);
refcode::clone_for(&repo_inv, &slug).ok_or_else(|| {
anyhow!("repo '{slug}' has no mapped clone here (`confer repos map {slug} <path>`)")
})?
}
None => {
let cwd = std::env::current_dir()?;
let o = gitcmd::output(&cwd, &["rev-parse", "--show-toplevel"])?;
if !o.status.success() {
return Err(anyhow!(
"not inside a git working tree — pass --repo <slug> to resolve via the clone map"
));
}
std::path::PathBuf::from(String::from_utf8_lossy(&o.stdout).trim())
}
};
if refcode::is_ancestor(&dir, &sha, &against) {
println!("{sha} is reachable from {against}");
Ok(())
} else {
println!("{sha} is NOT reachable from {against}");
Err(PredicateFalse.into())
}
}
pub(crate) fn render_refs(refs: &[schema::CodeRef]) -> String {
let Some(first) = refs.first() else {
return String::new();
};
let more = if refs.len() > 1 {
format!(" +{}", refs.len() - 1)
} else {
String::new()
};
let paren = refcode::identity_paren(
first.ref_name.as_deref(),
first.ref_type.as_deref(),
first.commit_date.as_deref(),
);
format!(" ⟶ {}:{}{paren}{more}", first.repo, first.path)
}