1use std::io::Write;
5use std::path::Path;
6
7use clap::{Parser, ValueEnum};
8use mkit_core::hash::Hash;
9use mkit_core::layout::RepoLayout;
10use mkit_core::object::Object;
11
12use crate::clap_shim;
13use crate::config;
14use crate::exit;
15use crate::format::{self, JsonObject};
16use crate::remote_dispatch;
17
18#[derive(Debug, Clone, Copy, ValueEnum)]
19enum PullFormat {
20 Default,
21 Json,
22}
23
24#[derive(Debug, Parser)]
25#[command(name = "mkit pull", about = "Pull changes from the configured remote.")]
26struct PullOpts {
27 remote: Option<String>,
29 #[arg(long = "no-verify-signatures")]
35 no_verify_signatures: bool,
36 #[arg(long, conflicts_with = "remote")]
41 all: bool,
42 #[arg(long, value_enum, default_value = "default")]
47 format: PullFormat,
48 #[arg(short = 'q', long)]
50 quiet: bool,
51}
52
53#[must_use]
54pub fn run(args: &[String]) -> u8 {
55 let opts = match clap_shim::parse::<PullOpts>("mkit pull", args) {
56 Ok(o) => o,
57 Err(code) => return code,
58 };
59 let json = matches!(opts.format, PullFormat::Json);
60 let cwd = match std::env::current_dir() {
61 Ok(p) => p,
62 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
63 };
64 let layout = match super::resolve_layout(&cwd) {
65 Ok(layout) => layout,
66 Err(code) => return code,
67 };
68 let cfg = match config::read_layered(&layout) {
69 Ok(c) => c,
70 Err(e) => return emit_err_json(&format!("config: {e}"), exit::CONFIG_ERROR, json),
71 };
72 let require_signed = !opts.no_verify_signatures && cfg.merged.pull_require_signed_or_default();
75 if opts.all {
76 let names = config::configured_remote_names(&cfg);
77 if names.is_empty() {
78 return emit_err_json(
79 "no remote configured — use `mkit remote add <url>`",
80 exit::CONFIG_ERROR,
81 json,
82 );
83 }
84 let mut worst = exit::OK;
88 for name in names {
89 let code = pull_one(&cwd, &layout, &cfg, &name, require_signed, json, opts.quiet);
90 if code != exit::OK {
91 worst = code;
92 }
93 }
94 return worst;
95 }
96 pull_one(
97 &cwd,
98 &layout,
99 &cfg,
100 opts.remote.as_deref().unwrap_or(""),
101 require_signed,
102 json,
103 opts.quiet,
104 )
105}
106
107fn pull_one(
112 cwd: &Path,
113 layout: &RepoLayout,
114 cfg: &config::LayeredConfig,
115 remote: &str,
116 require_signed: bool,
117 json: bool,
118 quiet: bool,
119) -> u8 {
120 let Some(resolved) = config::resolve_remote(cfg, remote) else {
121 return emit_err_json(
122 &if remote.is_empty() {
123 "no remote configured — use `mkit remote add <url>`".to_owned()
124 } else {
125 format!("unknown remote '{remote}'")
126 },
127 exit::CONFIG_ERROR,
128 json,
129 );
130 };
131 let endpoint = resolved.endpoint.as_str();
132 let branch = match mkit_core::refs::read_head(layout) {
136 Ok(mkit_core::refs::Head::Branch(b)) => Some(b),
137 _ => None,
138 };
139 let old_tip = branch
140 .as_deref()
141 .and_then(|b| mkit_core::refs::read_ref(layout, b).ok().flatten());
142 match remote_dispatch::open_trusted(endpoint, resolved.repo_chosen, cfg, layout) {
143 Ok(tx) => {
144 let pull_outcome = {
145 let _progress = crate::progress::start(
149 "Unpacking objects",
150 None,
151 crate::progress::should_report(quiet),
152 );
153 remote_dispatch::pull_all_with(
154 cwd,
155 tx.as_ref(),
156 &resolved.name,
157 None,
158 require_signed,
159 )
160 };
161 match pull_outcome {
162 Ok(_) => {
163 let new_tip = branch
164 .as_deref()
165 .and_then(|b| mkit_core::refs::read_ref(layout, b).ok().flatten());
166 report_pull(layout, endpoint, old_tip, new_tip);
167 if json {
168 let mut obj = JsonObject::new();
169 obj.field_bool("ok", true)
170 .field_str("remote", &resolved.name)
171 .field_str("endpoint", endpoint)
172 .field_opt_str("branch", branch.as_deref())
173 .field_opt_hash("old", old_tip.as_ref())
174 .field_opt_hash("new", new_tip.as_ref())
175 .field_bool("up_to_date", old_tip == new_tip);
176 let mut stdout = std::io::stdout().lock();
177 let _ = writeln!(stdout, "{}", obj.finish());
178 }
179 exit::OK
180 }
181 Err(remote_dispatch::DispatchError::Interrupted) => {
182 emit_err_json("pull: interrupted", exit::TEMPFAIL, json)
183 }
184 Err(e @ remote_dispatch::DispatchError::UnsignedOrInvalidObject { .. }) => {
185 emit_err_json(&format!("pull: {e}"), exit::DATAERR, json)
186 }
187 Err(e) => emit_err_json(&format!("pull: {e}"), exit::GENERAL_ERROR, json),
188 }
189 }
190 Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
191 emit_err_json(&msg, exit::CONFIG_ERROR, json)
192 }
193 Err(e) => emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
194 }
195}
196
197fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
200 if json {
201 let mut obj = JsonObject::new();
202 obj.field_bool("ok", false).field_str("error", msg);
203 let mut stdout = std::io::stdout().lock();
204 let _ = writeln!(stdout, "{}", obj.finish());
205 }
206 emit_err(msg, code)
207}
208
209fn report_pull(layout: &RepoLayout, endpoint: &str, old: Option<Hash>, new: Option<Hash>) {
214 let mut stderr = std::io::stderr().lock();
215 match (old, new) {
216 (o, n) if o == n => {
217 let _ = writeln!(stderr, "Already up to date.");
218 }
219 (Some(o), Some(n)) => {
220 let _ = writeln!(stderr, "From {endpoint}");
221 let _ = writeln!(
222 stderr,
223 "Updating {}..{}",
224 format::short_hash(&o, format::SUMMARY_ABBREV),
225 format::short_hash(&n, format::SUMMARY_ABBREV),
226 );
227 let _ = writeln!(stderr, "Fast-forward");
228 drop(stderr);
229 print_ff_stat(layout, o, n);
230 }
231 _ => {
232 }
236 }
237}
238
239fn print_ff_stat(layout: &RepoLayout, old: Hash, new: Hash) {
242 let Ok(store) = crate::commands::open_store_configured(layout) else {
243 return;
244 };
245 let (Some(old_tree), Some(new_tree)) = (tree_of(&store, old), tree_of(&store, new)) else {
246 return;
247 };
248 if let Ok(result) = mkit_core::ops::diff_trees(&store, Some(old_tree), Some(new_tree)) {
249 let mut stderr = std::io::stderr().lock();
250 let _ = super::diff::render_stat(&mut stderr, &store, result.entries.iter());
252 }
253}
254
255fn tree_of(store: &mkit_core::store::ObjectStore, commit: Hash) -> Option<Hash> {
256 match store.read_object(&commit).ok()? {
257 Object::Commit(c) => Some(c.tree_hash),
258 Object::Remix(r) => Some(r.tree_hash),
259 _ => None,
260 }
261}
262
263use super::error as emit_err;