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.name, resolved.repo_chosen, cfg, layout)
143 {
144 Ok(tx) => {
145 let pull_outcome = {
146 let _progress = crate::progress::start(
150 "Unpacking objects",
151 None,
152 crate::progress::should_report(quiet),
153 quiet,
154 );
155 remote_dispatch::pull_all_with(
156 cwd,
157 tx.as_ref(),
158 &resolved.name,
159 None,
160 require_signed,
161 )
162 };
163 match pull_outcome {
164 Ok(_) => {
165 let new_tip = branch
166 .as_deref()
167 .and_then(|b| mkit_core::refs::read_ref(layout, b).ok().flatten());
168 report_pull(layout, endpoint, old_tip, new_tip);
169 if json {
170 let mut obj = JsonObject::new();
171 obj.field_bool("ok", true)
172 .field_str("remote", &resolved.name)
173 .field_str("endpoint", endpoint)
174 .field_opt_str("branch", branch.as_deref())
175 .field_opt_hash("old", old_tip.as_ref())
176 .field_opt_hash("new", new_tip.as_ref())
177 .field_bool("up_to_date", old_tip == new_tip);
178 let mut stdout = std::io::stdout().lock();
179 let _ = writeln!(stdout, "{}", obj.finish());
180 }
181 exit::OK
182 }
183 Err(remote_dispatch::DispatchError::Interrupted) => {
184 emit_err_json("pull: interrupted", exit::TEMPFAIL, json)
185 }
186 Err(e @ remote_dispatch::DispatchError::UnsignedOrInvalidObject { .. }) => {
187 emit_err_json(&format!("pull: {e}"), exit::DATAERR, json)
188 }
189 Err(e) => emit_err_json(&format!("pull: {e}"), exit::GENERAL_ERROR, json),
190 }
191 }
192 Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
193 emit_err_json(&msg, exit::CONFIG_ERROR, json)
194 }
195 Err(e) => emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
196 }
197}
198
199fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
202 if json {
203 let mut obj = JsonObject::new();
204 obj.field_bool("ok", false).field_str("error", msg);
205 let mut stdout = std::io::stdout().lock();
206 let _ = writeln!(stdout, "{}", obj.finish());
207 }
208 emit_err(msg, code)
209}
210
211fn report_pull(layout: &RepoLayout, endpoint: &str, old: Option<Hash>, new: Option<Hash>) {
216 let mut stderr = std::io::stderr().lock();
217 match (old, new) {
218 (o, n) if o == n => {
219 let _ = writeln!(stderr, "Already up to date.");
220 }
221 (Some(o), Some(n)) => {
222 let _ = writeln!(stderr, "From {endpoint}");
223 let _ = writeln!(
224 stderr,
225 "Updating {}..{}",
226 format::short_hash(&o, format::SUMMARY_ABBREV),
227 format::short_hash(&n, format::SUMMARY_ABBREV),
228 );
229 let _ = writeln!(stderr, "Fast-forward");
230 drop(stderr);
231 print_ff_stat(layout, o, n);
232 }
233 _ => {
234 }
238 }
239}
240
241fn print_ff_stat(layout: &RepoLayout, old: Hash, new: Hash) {
244 let Ok(store) = crate::commands::open_store_configured(layout) else {
245 return;
246 };
247 let (Some(old_tree), Some(new_tree)) = (tree_of(&store, old), tree_of(&store, new)) else {
248 return;
249 };
250 if let Ok(result) = mkit_core::ops::diff_trees(&store, Some(old_tree), Some(new_tree)) {
251 let mut stderr = std::io::stderr().lock();
252 let _ = super::diff::render_stat(&mut stderr, &store, result.entries.iter());
254 }
255}
256
257fn tree_of(store: &mkit_core::store::ObjectStore, commit: Hash) -> Option<Hash> {
258 match store.read_object(&commit).ok()? {
259 Object::Commit(c) => Some(c.tree_hash),
260 Object::Remix(r) => Some(r.tree_hash),
261 _ => None,
262 }
263}
264
265use super::error as emit_err;