1use std::io::Write;
14
15use clap::{Parser, ValueEnum};
16use mkit_core::layout::RepoLayout;
17
18use crate::clap_shim;
19use crate::config;
20use crate::exit;
21use crate::format::JsonObject;
22use crate::remote_dispatch::{self, PushLease};
23
24#[derive(Debug, Clone, Copy, ValueEnum)]
25enum PushFormat {
26 Default,
27 Json,
28}
29
30#[derive(Debug, Parser)]
31#[command(
32 name = "mkit push",
33 about = "Push the current branch to its upstream (or --all branches)."
34)]
35#[allow(clippy::struct_excessive_bools)]
36struct PushOpts {
37 remote: Option<String>,
40 #[arg(long)]
42 all: bool,
43 #[arg(short = 'f', long)]
45 force: bool,
46 #[arg(short = 'u', long = "set-upstream")]
49 set_upstream: bool,
50 #[arg(long)]
52 force_with_lease: bool,
53 #[arg(long)]
55 dry_run: bool,
56 #[arg(long, value_enum, default_value = "default")]
63 format: PushFormat,
64 #[arg(short = 'q', long)]
66 quiet: bool,
67}
68
69#[must_use]
70pub fn run(args: &[String]) -> u8 {
71 let opts = match clap_shim::parse::<PushOpts>("mkit push", args) {
72 Ok(o) => o,
73 Err(code) => return code,
74 };
75 if opts.force && opts.force_with_lease {
76 return emit_err(
77 "--force and --force-with-lease are mutually exclusive",
78 exit::USAGE,
79 );
80 }
81 let cwd = match std::env::current_dir() {
82 Ok(p) => p,
83 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
84 };
85 let layout = match super::resolve_layout(&cwd) {
86 Ok(layout) => layout,
87 Err(code) => return code,
88 };
89 let cfg = match config::read_layered(&layout) {
90 Ok(c) => c,
91 Err(e) => return emit_err(&format!("config: {e}"), exit::CONFIG_ERROR),
92 };
93
94 if opts.all {
95 push_all(&layout, &cfg, &opts)
96 } else {
97 push_current(&layout, &cfg, &opts)
98 }
99}
100
101#[allow(clippy::too_many_lines)] fn push_current(layout: &RepoLayout, cfg: &config::LayeredConfig, opts: &PushOpts) -> u8 {
104 let json = matches!(opts.format, PushFormat::Json);
105 let branch = match mkit_core::refs::read_head(layout) {
106 Ok(mkit_core::refs::Head::Branch(b)) => b,
107 Ok(mkit_core::refs::Head::Detached(_)) => {
108 return emit_err_json(
109 "cannot push a detached HEAD; check out a branch first",
110 exit::CONFIG_ERROR,
111 json,
112 );
113 }
114 Err(e) => return emit_err_json(&format!("read HEAD: {e}"), exit::CONFIG_ERROR, json),
115 };
116
117 let (remote_name, remote_branch) = match &opts.remote {
121 Some(name) => (name.clone(), branch.clone()),
122 None => match config::resolve_upstream(cfg, &branch) {
123 Some(up) => (up.remote, up.branch),
124 None => {
125 return emit_err_json(
126 &format!(
127 "no upstream configured for branch '{branch}' and no default remote; \
128 run `mkit push <remote>` to push it (the upstream will be remembered)"
129 ),
130 exit::CONFIG_ERROR,
131 json,
132 );
133 }
134 },
135 };
136
137 let Some(resolved) = config::resolve_remote(cfg, &remote_name) else {
138 return emit_err_json(
139 &format!(
140 "unknown remote '{remote_name}' — add it with `mkit remote add {remote_name} <url>`"
141 ),
142 exit::CONFIG_ERROR,
143 json,
144 );
145 };
146
147 let local_tip = mkit_core::refs::read_ref(layout, &branch).ok().flatten();
150 let old_tracked = mkit_core::refs::read_remote_ref(layout, &resolved.name, &remote_branch)
151 .ok()
152 .flatten();
153 if !opts.force && local_tip.is_some() && local_tip == old_tracked {
156 let mut stderr = std::io::stderr().lock();
157 let _ = writeln!(stderr, "Everything up-to-date");
158 if json {
159 let mut obj = JsonObject::new();
160 obj.field_bool("ok", true)
161 .field_str("remote", &resolved.name)
162 .field_str("endpoint", &resolved.endpoint)
163 .field_str("branch", &branch)
164 .field_str("remote_branch", &remote_branch)
165 .field_opt_hash("old", old_tracked.as_ref())
166 .field_opt_hash("new", old_tracked.as_ref())
167 .field_bool("forced", false)
168 .field_bool("up_to_date", true);
169 emit_json_stdout(obj);
170 }
171 return exit::OK;
172 }
173
174 let lease = lease_for(opts);
175 if opts.dry_run {
176 let mut stderr = std::io::stderr().lock();
177 let _ = writeln!(
178 stderr,
179 "(dry-run) would push {branch} -> {}:{remote_branch} ({})",
180 resolved.name, resolved.endpoint
181 );
182 if json {
183 let mut obj = JsonObject::new();
184 obj.field_bool("ok", true)
185 .field_bool("dry_run", true)
186 .field_str("remote", &resolved.name)
187 .field_str("endpoint", &resolved.endpoint)
188 .field_str("branch", &branch)
189 .field_str("remote_branch", &remote_branch);
190 emit_json_stdout(obj);
191 }
192 return exit::OK;
193 }
194
195 let tx = match remote_dispatch::open_trusted(
196 &resolved.endpoint,
197 resolved.repo_chosen,
198 cfg,
199 layout,
200 ) {
201 Ok(tx) => tx,
202 Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
203 return emit_err_json(&msg, exit::CONFIG_ERROR, json);
204 }
205 Err(e) => return emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
206 };
207
208 let push_outcome = {
209 let _progress = crate::progress::start(
213 "Writing objects",
214 None,
215 crate::progress::should_report(opts.quiet),
216 );
217 remote_dispatch::push_branch_tracked(
218 layout.worktree_root(),
219 tx.as_ref(),
220 &resolved.name,
221 &branch,
222 &remote_branch,
223 lease,
224 )
225 };
226 match push_outcome {
227 Ok(new_tip) => {
228 record_upstream(
233 layout,
234 cfg,
235 &branch,
236 &resolved.name,
237 &remote_branch,
238 opts.set_upstream,
239 );
240 let forced =
245 !remote_dispatch::is_fast_forward(layout.worktree_root(), old_tracked, new_tip)
246 .unwrap_or(true);
247 let mut stderr = std::io::stderr().lock();
248 let _ = writeln!(stderr, "To {}", resolved.endpoint);
249 let _ = writeln!(
250 stderr,
251 "{}",
252 crate::format::ref_update_line(
253 old_tracked.as_ref(),
254 &new_tip,
255 &branch,
256 &remote_branch,
257 forced,
258 )
259 );
260 if json {
261 let mut obj = JsonObject::new();
262 obj.field_bool("ok", true)
263 .field_str("remote", &resolved.name)
264 .field_str("endpoint", &resolved.endpoint)
265 .field_str("branch", &branch)
266 .field_str("remote_branch", &remote_branch)
267 .field_opt_hash("old", old_tracked.as_ref())
268 .field_hash("new", &new_tip)
269 .field_bool("forced", forced)
270 .field_bool("up_to_date", false);
271 emit_json_stdout(obj);
272 }
273 exit::OK
274 }
275 Err(remote_dispatch::DispatchError::NonFastForwardPush { branch: rejected }) => {
276 let mut stderr = std::io::stderr().lock();
277 let _ = writeln!(stderr, "To {}", resolved.endpoint);
278 let _ = writeln!(
279 stderr,
280 "{}",
281 crate::format::ref_rejected_line(&rejected, &rejected)
282 );
283 drop(stderr);
284 let msg = format!(
285 "updates were rejected for '{rejected}' (non-fast-forward); \
286 `mkit fetch` and merge/rebase first, or re-run with --force-with-lease / --force"
287 );
288 if json {
289 let mut obj = JsonObject::new();
290 obj.field_bool("ok", false)
291 .field_bool("rejected", true)
292 .field_str("remote", &resolved.name)
293 .field_str("endpoint", &resolved.endpoint)
294 .field_str("branch", &rejected)
295 .field_str("remote_branch", &remote_branch)
296 .field_str("error", &msg);
297 emit_json_stdout(obj);
298 }
299 emit_err(&msg, exit::GENERAL_ERROR)
300 }
301 Err(remote_dispatch::DispatchError::Interrupted) => {
302 emit_err_json("push: interrupted", exit::TEMPFAIL, json)
303 }
304 Err(e) => emit_err_json(&format!("push: {e}"), exit::GENERAL_ERROR, json),
305 }
306}
307
308fn push_all(layout: &RepoLayout, cfg: &config::LayeredConfig, opts: &PushOpts) -> u8 {
310 let json = matches!(opts.format, PushFormat::Json);
311 let remote_name = opts
312 .remote
313 .clone()
314 .unwrap_or_else(|| config::DEFAULT_REMOTE_NAME.to_owned());
315 let Some(resolved) = config::resolve_remote(cfg, &remote_name) else {
316 return emit_err_json(
317 "no remote configured — use `mkit remote add <url>`",
318 exit::CONFIG_ERROR,
319 json,
320 );
321 };
322 if opts.dry_run {
323 let mut stderr = std::io::stderr().lock();
324 let _ = writeln!(
325 stderr,
326 "(dry-run) would mirror all branches to {} ({})",
327 resolved.name, resolved.endpoint
328 );
329 if json {
330 let mut obj = JsonObject::new();
331 obj.field_bool("ok", true)
332 .field_bool("dry_run", true)
333 .field_str("remote", &resolved.name)
334 .field_str("endpoint", &resolved.endpoint);
335 emit_json_stdout(obj);
336 }
337 return exit::OK;
338 }
339 let tx = match remote_dispatch::open_trusted(
340 &resolved.endpoint,
341 resolved.repo_chosen,
342 cfg,
343 layout,
344 ) {
345 Ok(tx) => tx,
346 Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
347 return emit_err_json(&msg, exit::CONFIG_ERROR, json);
348 }
349 Err(e) => return emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
350 };
351 let push_outcome = {
352 let _progress = crate::progress::start(
353 "Writing objects",
354 None,
355 crate::progress::should_report(opts.quiet),
356 );
357 remote_dispatch::push_all_with(
358 layout.worktree_root(),
359 tx.as_ref(),
360 Some(&resolved.name),
361 opts.force,
362 )
363 };
364 match push_outcome {
365 Ok(n) => {
366 let mut stderr = std::io::stderr().lock();
367 let _ = writeln!(
368 stderr,
369 "pushed {n} ref(s) to {} ({})",
370 resolved.name, resolved.endpoint
371 );
372 if json {
373 let mut obj = JsonObject::new();
374 obj.field_bool("ok", true)
375 .field_str("remote", &resolved.name)
376 .field_str("endpoint", &resolved.endpoint)
377 .field_u64("ref_count", n as u64);
378 emit_json_stdout(obj);
379 }
380 exit::OK
381 }
382 Err(remote_dispatch::DispatchError::NonFastForwardPush { branch }) => {
383 let msg = format!(
384 "updates were rejected for '{branch}' (non-fast-forward); \
385 `mkit fetch` first, or re-run with --force"
386 );
387 if json {
388 let mut obj = JsonObject::new();
389 obj.field_bool("ok", false)
390 .field_bool("rejected", true)
391 .field_str("remote", &resolved.name)
392 .field_str("endpoint", &resolved.endpoint)
393 .field_str("branch", &branch)
394 .field_str("error", &msg);
395 emit_json_stdout(obj);
396 }
397 emit_err(&msg, exit::GENERAL_ERROR)
398 }
399 Err(remote_dispatch::DispatchError::Interrupted) => {
400 emit_err_json("push: interrupted", exit::TEMPFAIL, json)
401 }
402 Err(e) => emit_err_json(&format!("push: {e}"), exit::GENERAL_ERROR, json),
403 }
404}
405
406fn emit_json_stdout(obj: JsonObject) {
408 let mut stdout = std::io::stdout().lock();
409 let _ = writeln!(stdout, "{}", obj.finish());
410}
411
412fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
417 if json {
418 let mut obj = JsonObject::new();
419 obj.field_bool("ok", false).field_str("error", msg);
420 emit_json_stdout(obj);
421 }
422 emit_err(msg, code)
423}
424
425fn lease_for(opts: &PushOpts) -> PushLease {
426 if opts.force {
427 PushLease::Force
428 } else if opts.force_with_lease {
429 PushLease::WithLease
430 } else {
431 PushLease::FastForward
432 }
433}
434
435fn record_upstream(
439 layout: &RepoLayout,
440 cfg: &config::LayeredConfig,
441 branch: &str,
442 remote: &str,
443 remote_branch: &str,
444 force: bool,
445) {
446 if !force
449 && cfg
450 .merged
451 .branch_upstreams
452 .get(branch)
453 .is_some_and(|u| !u.remote.is_empty())
454 {
455 return;
456 }
457 let Ok(layered) = config::read_layered(layout) else {
462 return;
463 };
464 let mut on_disk = layered.repo;
465 on_disk.branch_upstreams.insert(
466 branch.to_owned(),
467 config::Upstream {
468 remote: remote.to_owned(),
469 branch: remote_branch.to_owned(),
470 },
471 );
472 let _ = config::write(layout, &on_disk);
473}
474
475use super::error as emit_err;