a73x

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,
    })
}