src/server/refs.rs
Ref: Size: 6.7 KiB History
//! `git-collab-server refs`: what collab refs the hosted repositories hold.
//!
//! `migrate --dry-run` already answers "what would change here", but only for
//! the repositories it can write, and only in the vocabulary of a migration. An
//! operator sequencing the removal of the legacy compatibility code (issue
//! e5096ffc) has a narrower question — *does any repository on this server still
//! hold a superseded shape?* — and needs it answered without the tool being
//! allowed to change the answer.
//!
//! So this is the read half of `migrate`: the same enumeration, no lock, no
//! writes, not even the writability probe. It reports a repository it cannot
//! open rather than skipping it, and exits non-zero when there was one, because
//! an audit that silently omits a repository reports "nothing legacy" for a
//! roster it did not finish reading.
//!
//! It summarises rather than dumping every ref. A roster's worth of full ref
//! names is not something anybody reads, and the actionable subset is the
//! legacy ones — those are named in full, under `--json`, so the answer can be
//! acted on directly. For the whole listing of one repository, run
//! `git-collab refs` in a clone of it.
use std::path::Path;
use git_collab::refs::{self, CollabRef};
use crate::repos;
/// What one repository had to say.
enum Outcome {
Scanned(Vec<CollabRef>),
/// Could not be read, and why. Never silently skipped.
Unreadable(String),
}
pub fn run(repos_dir: &Path, json: bool) -> i32 {
let entries = match repos::discover(repos_dir) {
Ok(entries) => entries,
Err(e) => {
eprintln!("error: cannot read {}: {}", repos_dir.display(), e);
return 1;
}
};
let scanned: Vec<(String, Outcome)> = entries
.iter()
.map(|entry| (entry.name.clone(), scan_one(entry)))
.collect();
let unreadable = scanned
.iter()
.filter(|(_, o)| matches!(o, Outcome::Unreadable(_)))
.count();
if json {
println!(
"{}",
serde_json::to_string_pretty(&as_json(&scanned)).unwrap()
);
} else {
render(repos_dir, &scanned);
}
if unreadable > 0 {
1
} else {
0
}
}
fn scan_one(entry: &repos::RepoEntry) -> Outcome {
match repos::open(entry).map_err(|e| e.to_string()) {
Err(e) => Outcome::Unreadable(format!("cannot open the repository: {}", e)),
Ok(repo) => match refs::scan(&repo) {
Ok(found) => Outcome::Scanned(found),
Err(e) => Outcome::Unreadable(format!("cannot read its refs: {}", e)),
},
}
}
fn render(repos_dir: &Path, scanned: &[(String, Outcome)]) {
if scanned.is_empty() {
println!("No repositories found under {}.", repos_dir.display());
return;
}
let mut legacy_repos = 0usize;
let mut unreadable = 0usize;
for (name, outcome) in scanned {
match outcome {
Outcome::Unreadable(reason) => {
unreadable += 1;
println!("{}: cannot be read — {}", name, reason);
}
Outcome::Scanned(found) if found.is_empty() => {
println!("{}: no collab refs", name);
}
Outcome::Scanned(found) => {
let legacy = refs::legacy_count(found);
if legacy > 0 {
legacy_repos += 1;
println!(
"{}: {} collab refs — {}; {} in an older layout ({})",
name,
found.len(),
refs::groups(found),
legacy,
refs::legacy_breakdown(found)
);
} else {
println!(
"{}: {} collab refs — {}",
name,
found.len(),
refs::groups(found)
);
}
}
}
}
let mut tail = Vec::new();
if legacy_repos > 0 {
tail.push(format!(
"{} hold{} an older layout",
legacy_repos,
if legacy_repos == 1 { "s" } else { "" }
));
}
if unreadable > 0 {
tail.push(format!("{} could not be read", unreadable));
}
if tail.is_empty() {
tail.push("all current".to_string());
}
println!(
"\n{} repositor{} scanned: {}.",
scanned.len(),
if scanned.len() == 1 { "y" } else { "ies" },
tail.join(", ")
);
}
fn as_json(scanned: &[(String, Outcome)]) -> serde_json::Value {
let repositories: Vec<serde_json::Value> = scanned
.iter()
.map(|(name, outcome)| match outcome {
Outcome::Unreadable(reason) => serde_json::json!({
"repo": name,
"error": reason,
}),
Outcome::Scanned(found) => {
// Full ref names and full ids: this is the list an operator
// acts on, and it has to be usable without a second lookup.
let legacy_refs: Vec<serde_json::Value> = found
.iter()
.filter(|r| r.is_legacy())
.map(|r| {
serde_json::json!({
"ref": r.name,
"target": r.target,
"kind": r.kind.slug(),
"id": r.id,
})
})
.collect();
let counts: serde_json::Map<String, serde_json::Value> = refs::counts(found)
.into_iter()
.map(|(k, v)| (k.to_string(), serde_json::Value::from(v)))
.collect();
serde_json::json!({
"repo": name,
"total": found.len(),
"legacy": legacy_refs.len(),
"counts": counts,
"legacy_refs": legacy_refs,
})
}
})
.collect();
let total: usize = scanned
.iter()
.filter_map(|(_, o)| match o {
Outcome::Scanned(found) => Some(found.len()),
Outcome::Unreadable(_) => None,
})
.sum();
let legacy: usize = scanned
.iter()
.filter_map(|(_, o)| match o {
Outcome::Scanned(found) => Some(refs::legacy_count(found)),
Outcome::Unreadable(_) => None,
})
.sum();
let unreadable = scanned
.iter()
.filter(|(_, o)| matches!(o, Outcome::Unreadable(_)))
.count();
serde_json::json!({
"repositories": repositories,
"total": total,
"legacy": legacy,
"unreadable": unreadable,
})
}