use std::io::Write;
use clap::{Parser, ValueEnum};
use mkit_core::layout::RepoLayout;
use crate::clap_shim;
use crate::config;
use crate::exit;
use crate::format::JsonObject;
use crate::remote_dispatch::{self, PushLease};
#[derive(Debug, Clone, Copy, ValueEnum)]
enum PushFormat {
Default,
Json,
}
#[derive(Debug, Parser)]
#[command(
name = "mkit push",
about = "Push the current branch to its upstream (or --all branches)."
)]
#[allow(clippy::struct_excessive_bools)]
struct PushOpts {
remote: Option<String>,
#[arg(long)]
all: bool,
#[arg(short = 'f', long)]
force: bool,
#[arg(short = 'u', long = "set-upstream")]
set_upstream: bool,
#[arg(long)]
force_with_lease: bool,
#[arg(long)]
dry_run: bool,
#[arg(long, value_enum, default_value = "default")]
format: PushFormat,
#[arg(short = 'q', long)]
quiet: bool,
}
#[must_use]
pub fn run(args: &[String]) -> u8 {
let opts = match clap_shim::parse::<PushOpts>("mkit push", args) {
Ok(o) => o,
Err(code) => return code,
};
if opts.force && opts.force_with_lease {
return emit_err(
"--force and --force-with-lease are mutually exclusive",
exit::USAGE,
);
}
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(&format!("config: {e}"), exit::CONFIG_ERROR),
};
if opts.all {
push_all(&layout, &cfg, &opts)
} else {
push_current(&layout, &cfg, &opts)
}
}
#[allow(clippy::too_many_lines)] fn push_current(layout: &RepoLayout, cfg: &config::LayeredConfig, opts: &PushOpts) -> u8 {
let json = matches!(opts.format, PushFormat::Json);
let branch = match mkit_core::refs::read_head(layout) {
Ok(mkit_core::refs::Head::Branch(b)) => b,
Ok(mkit_core::refs::Head::Detached(_)) => {
return emit_err_json(
"cannot push a detached HEAD; check out a branch first",
exit::CONFIG_ERROR,
json,
);
}
Err(e) => return emit_err_json(&format!("read HEAD: {e}"), exit::CONFIG_ERROR, json),
};
let (remote_name, remote_branch) = match &opts.remote {
Some(name) => (name.clone(), branch.clone()),
None => match config::resolve_upstream(cfg, &branch) {
Some(up) => (up.remote, up.branch),
None => {
return emit_err_json(
&format!(
"no upstream configured for branch '{branch}' and no default remote; \
run `mkit push <remote>` to push it (the upstream will be remembered)"
),
exit::CONFIG_ERROR,
json,
);
}
},
};
let Some(resolved) = config::resolve_remote(cfg, &remote_name) else {
return emit_err_json(
&format!(
"unknown remote '{remote_name}' — add it with `mkit remote add {remote_name} <url>`"
),
exit::CONFIG_ERROR,
json,
);
};
let local_tip = mkit_core::refs::read_ref(layout, &branch).ok().flatten();
let old_tracked = mkit_core::refs::read_remote_ref(layout, &resolved.name, &remote_branch)
.ok()
.flatten();
if !opts.force && local_tip.is_some() && local_tip == old_tracked {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "Everything up-to-date");
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("remote", &resolved.name)
.field_str("endpoint", &resolved.endpoint)
.field_str("branch", &branch)
.field_str("remote_branch", &remote_branch)
.field_opt_hash("old", old_tracked.as_ref())
.field_opt_hash("new", old_tracked.as_ref())
.field_bool("forced", false)
.field_bool("up_to_date", true);
emit_json_stdout(obj);
}
return exit::OK;
}
let lease = lease_for(opts);
if opts.dry_run {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"(dry-run) would push {branch} -> {}:{remote_branch} ({})",
resolved.name, resolved.endpoint
);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_bool("dry_run", true)
.field_str("remote", &resolved.name)
.field_str("endpoint", &resolved.endpoint)
.field_str("branch", &branch)
.field_str("remote_branch", &remote_branch);
emit_json_stdout(obj);
}
return exit::OK;
}
let tx = match remote_dispatch::open_trusted(
&resolved.endpoint,
resolved.repo_chosen,
cfg,
layout,
) {
Ok(tx) => tx,
Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
return emit_err_json(&msg, exit::CONFIG_ERROR, json);
}
Err(e) => return emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
};
let push_outcome = {
let _progress = crate::progress::start(
"Writing objects",
None,
crate::progress::should_report(opts.quiet),
);
remote_dispatch::push_branch_tracked(
layout.worktree_root(),
tx.as_ref(),
&resolved.name,
&branch,
&remote_branch,
lease,
)
};
match push_outcome {
Ok(new_tip) => {
record_upstream(
layout,
cfg,
&branch,
&resolved.name,
&remote_branch,
opts.set_upstream,
);
let forced =
!remote_dispatch::is_fast_forward(layout.worktree_root(), old_tracked, new_tip)
.unwrap_or(true);
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "To {}", resolved.endpoint);
let _ = writeln!(
stderr,
"{}",
crate::format::ref_update_line(
old_tracked.as_ref(),
&new_tip,
&branch,
&remote_branch,
forced,
)
);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("remote", &resolved.name)
.field_str("endpoint", &resolved.endpoint)
.field_str("branch", &branch)
.field_str("remote_branch", &remote_branch)
.field_opt_hash("old", old_tracked.as_ref())
.field_hash("new", &new_tip)
.field_bool("forced", forced)
.field_bool("up_to_date", false);
emit_json_stdout(obj);
}
exit::OK
}
Err(remote_dispatch::DispatchError::NonFastForwardPush { branch: rejected }) => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "To {}", resolved.endpoint);
let _ = writeln!(
stderr,
"{}",
crate::format::ref_rejected_line(&rejected, &rejected)
);
drop(stderr);
let msg = format!(
"updates were rejected for '{rejected}' (non-fast-forward); \
`mkit fetch` and merge/rebase first, or re-run with --force-with-lease / --force"
);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", false)
.field_bool("rejected", true)
.field_str("remote", &resolved.name)
.field_str("endpoint", &resolved.endpoint)
.field_str("branch", &rejected)
.field_str("remote_branch", &remote_branch)
.field_str("error", &msg);
emit_json_stdout(obj);
}
emit_err(&msg, exit::GENERAL_ERROR)
}
Err(remote_dispatch::DispatchError::Interrupted) => {
emit_err_json("push: interrupted", exit::TEMPFAIL, json)
}
Err(e) => emit_err_json(&format!("push: {e}"), exit::GENERAL_ERROR, json),
}
}
fn push_all(layout: &RepoLayout, cfg: &config::LayeredConfig, opts: &PushOpts) -> u8 {
let json = matches!(opts.format, PushFormat::Json);
let remote_name = opts
.remote
.clone()
.unwrap_or_else(|| config::DEFAULT_REMOTE_NAME.to_owned());
let Some(resolved) = config::resolve_remote(cfg, &remote_name) else {
return emit_err_json(
"no remote configured — use `mkit remote add <url>`",
exit::CONFIG_ERROR,
json,
);
};
if opts.dry_run {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"(dry-run) would mirror all branches to {} ({})",
resolved.name, resolved.endpoint
);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_bool("dry_run", true)
.field_str("remote", &resolved.name)
.field_str("endpoint", &resolved.endpoint);
emit_json_stdout(obj);
}
return exit::OK;
}
let tx = match remote_dispatch::open_trusted(
&resolved.endpoint,
resolved.repo_chosen,
cfg,
layout,
) {
Ok(tx) => tx,
Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
return emit_err_json(&msg, exit::CONFIG_ERROR, json);
}
Err(e) => return emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
};
let push_outcome = {
let _progress = crate::progress::start(
"Writing objects",
None,
crate::progress::should_report(opts.quiet),
);
remote_dispatch::push_all_with(
layout.worktree_root(),
tx.as_ref(),
Some(&resolved.name),
opts.force,
)
};
match push_outcome {
Ok(n) => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"pushed {n} ref(s) to {} ({})",
resolved.name, resolved.endpoint
);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("remote", &resolved.name)
.field_str("endpoint", &resolved.endpoint)
.field_u64("ref_count", n as u64);
emit_json_stdout(obj);
}
exit::OK
}
Err(remote_dispatch::DispatchError::NonFastForwardPush { branch }) => {
let msg = format!(
"updates were rejected for '{branch}' (non-fast-forward); \
`mkit fetch` first, or re-run with --force"
);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", false)
.field_bool("rejected", true)
.field_str("remote", &resolved.name)
.field_str("endpoint", &resolved.endpoint)
.field_str("branch", &branch)
.field_str("error", &msg);
emit_json_stdout(obj);
}
emit_err(&msg, exit::GENERAL_ERROR)
}
Err(remote_dispatch::DispatchError::Interrupted) => {
emit_err_json("push: interrupted", exit::TEMPFAIL, json)
}
Err(e) => emit_err_json(&format!("push: {e}"), exit::GENERAL_ERROR, json),
}
}
fn emit_json_stdout(obj: JsonObject) {
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.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);
emit_json_stdout(obj);
}
emit_err(msg, code)
}
fn lease_for(opts: &PushOpts) -> PushLease {
if opts.force {
PushLease::Force
} else if opts.force_with_lease {
PushLease::WithLease
} else {
PushLease::FastForward
}
}
fn record_upstream(
layout: &RepoLayout,
cfg: &config::LayeredConfig,
branch: &str,
remote: &str,
remote_branch: &str,
force: bool,
) {
if !force
&& cfg
.merged
.branch_upstreams
.get(branch)
.is_some_and(|u| !u.remote.is_empty())
{
return;
}
let Ok(layered) = config::read_layered(layout) else {
return;
};
let mut on_disk = layered.repo;
on_disk.branch_upstreams.insert(
branch.to_owned(),
config::Upstream {
remote: remote.to_owned(),
branch: remote_branch.to_owned(),
},
);
let _ = config::write(layout, &on_disk);
}
use super::error as emit_err;