use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use clap::{Parser, ValueEnum};
use mkit_core::hash::Hash;
use mkit_core::layout::RepoLayout;
use crate::clap_shim;
use crate::config;
use crate::exit;
use crate::format::{self, JsonObject};
use crate::remote_dispatch;
#[derive(Debug, Clone, Copy, ValueEnum)]
enum FetchFormat {
Default,
Json,
}
#[derive(Debug, Parser)]
#[command(
name = "mkit fetch",
about = "Download from the configured remote without merging."
)]
struct FetchOpts {
remote: Option<String>,
#[arg(long = "no-verify-signatures")]
no_verify_signatures: bool,
#[arg(long, conflicts_with = "remote")]
all: bool,
#[arg(long, value_enum, default_value = "default")]
format: FetchFormat,
#[arg(short = 'q', long)]
quiet: bool,
}
#[must_use]
pub fn run(args: &[String]) -> u8 {
let opts = match clap_shim::parse::<FetchOpts>("mkit fetch", args) {
Ok(o) => o,
Err(code) => return code,
};
let json = matches!(opts.format, FetchFormat::Json);
let cwd = match std::env::current_dir() {
Ok(p) => p,
Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
};
let layout = match super::resolve_layout(&cwd) {
Ok(layout) => layout,
Err(code) => return code,
};
let cfg = match config::read_layered(&layout) {
Ok(c) => c,
Err(e) => return emit_err_json(&format!("config: {e}"), exit::CONFIG_ERROR, json),
};
let require_signed = !opts.no_verify_signatures && cfg.merged.pull_require_signed_or_default();
if opts.all {
let names = config::configured_remote_names(&cfg);
if names.is_empty() {
return emit_err_json(
"no remote configured — use `mkit remote add <url>`",
exit::CONFIG_ERROR,
json,
);
}
let mut worst = exit::OK;
for name in names {
let code = fetch_one(&cwd, &layout, &cfg, &name, require_signed, json, opts.quiet);
if code != exit::OK {
worst = code;
}
}
return worst;
}
fetch_one(
&cwd,
&layout,
&cfg,
opts.remote.as_deref().unwrap_or(""),
require_signed,
json,
opts.quiet,
)
}
fn fetch_one(
cwd: &Path,
layout: &RepoLayout,
cfg: &config::LayeredConfig,
remote: &str,
require_signed: bool,
json: bool,
quiet: bool,
) -> u8 {
let Some(resolved) = config::resolve_remote(cfg, remote) else {
return emit_err_json(
&if remote.is_empty() {
"no remote configured — use `mkit remote add <url>`".to_owned()
} else {
format!("unknown remote '{remote}'")
},
exit::CONFIG_ERROR,
json,
);
};
let endpoint = resolved.endpoint.as_str();
let before = tracking_snapshot(layout, &resolved.name);
match remote_dispatch::open_trusted(endpoint, resolved.repo_chosen, cfg, layout) {
Ok(tx) => {
let fetch_outcome = {
let _progress = crate::progress::start(
"Unpacking objects",
None,
crate::progress::should_report(quiet),
);
remote_dispatch::fetch_all_with(cwd, tx.as_ref(), &resolved.name, require_signed)
};
match fetch_outcome {
Ok(_) => {
let after = tracking_snapshot(layout, &resolved.name);
report_fetch(endpoint, &resolved.name, &before, &after);
if json {
emit_fetch_json(&resolved.name, endpoint, &before, &after);
}
exit::OK
}
Err(remote_dispatch::DispatchError::Interrupted) => {
emit_err_json("fetch: interrupted", exit::TEMPFAIL, json)
}
Err(e @ remote_dispatch::DispatchError::UnsignedOrInvalidObject { .. }) => {
emit_err_json(&format!("fetch: {e}"), exit::DATAERR, json)
}
Err(e) => emit_err_json(&format!("fetch: {e}"), exit::GENERAL_ERROR, json),
}
}
Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
emit_err_json(&msg, exit::CONFIG_ERROR, json)
}
Err(e) => emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
}
}
fn emit_fetch_json(
remote: &str,
endpoint: &str,
before: &HashMap<String, Hash>,
after: &HashMap<String, Hash>,
) {
let mut changed: Vec<(&String, Option<Hash>, Hash)> = after
.iter()
.filter(|(name, new)| before.get(*name) != Some(*new))
.map(|(name, new)| (name, before.get(name).copied(), *new))
.collect();
changed.sort_by(|a, b| a.0.cmp(b.0));
let entries: Vec<String> = changed
.iter()
.map(|(name, old, new)| {
let mut obj = JsonObject::new();
obj.field_str("name", name)
.field_opt_hash("old", old.as_ref())
.field_hash("new", new);
obj.finish()
})
.collect();
let mut top = JsonObject::new();
top.field_bool("ok", true)
.field_str("remote", remote)
.field_str("endpoint", endpoint)
.field_raw("updated", &format!("[{}]", entries.join(",")));
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", top.finish());
}
fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", false).field_str("error", msg);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
emit_err(msg, code)
}
fn tracking_snapshot(layout: &RepoLayout, remote: &str) -> HashMap<String, Hash> {
mkit_core::refs::list_remote_refs(layout, remote)
.unwrap_or_default()
.into_iter()
.filter_map(|r| r.hash.map(|h| (r.name, h)))
.collect()
}
fn report_fetch(
endpoint: &str,
remote: &str,
before: &HashMap<String, Hash>,
after: &HashMap<String, Hash>,
) {
let mut changed: Vec<(&String, Option<Hash>, Hash)> = after
.iter()
.filter(|(name, new)| before.get(*name) != Some(*new))
.map(|(name, new)| (name, before.get(name).copied(), *new))
.collect();
if changed.is_empty() {
return;
}
changed.sort_by(|a, b| a.0.cmp(b.0));
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "From {endpoint}");
for (name, old, new) in changed {
let dst = format!("{remote}/{name}");
let _ = writeln!(
stderr,
"{}",
format::ref_update_line(old.as_ref(), &new, name, &dst, false)
);
}
}
use super::error as emit_err;