use mnem_core::id::Cid;
use mnem_core::objects::RefTarget;
use mnem_transport::HttpRemoteClient;
use mnem_transport::client::RemoteClient;
use mnem_transport::error::ClientError;
use mnem_transport::export::export;
use mnem_transport::remote::parse_config;
use mnem_transport::secret_token::SecretToken;
use super::*;
#[derive(clap::Args, Debug)]
#[command(after_long_help = "\
Examples:
mnem push # push HEAD to origin/main
mnem push origin main
MNEM_REMOTE_ORIGIN_TOKEN=... mnem push origin main
")]
pub(crate) struct Args {
pub remote: Option<String>,
pub branch: Option<String>,
}
pub(crate) fn run(override_path: Option<&Path>, args: Args) -> Result<()> {
let remote_name = args.remote.as_deref().unwrap_or("origin").to_string();
let branch = args.branch.as_deref().unwrap_or("main").to_string();
let (data_dir, repo, bs, _ohs) = repo::open_all(override_path)?;
let cfg_path = data_dir.join(config::CONFIG_FILE);
let section = if cfg_path.exists() {
let cfg_text = std::fs::read_to_string(&cfg_path)
.with_context(|| format!("reading {}", cfg_path.display()))?;
parse_config(&cfg_text).with_context(|| format!("parsing {}", cfg_path.display()))?
} else {
mnem_transport::RemoteSection::default()
};
let file = section.remote.get(&remote_name).ok_or_else(|| {
anyhow!(
"no remote `{remote_name}` configured; run `mnem remote add {remote_name} <url>` first"
)
})?;
let token = resolve_token(&remote_name, file.token_env.as_deref()).ok_or_else(|| {
let upper = remote_name.to_ascii_uppercase();
anyhow!(
"Authentication required. Set MNEM_REMOTE_{upper}_TOKEN env var \
(or MNEM_HTTP_PUSH_TOKEN) to push to `{remote_name}`."
)
})?;
let mut cfg = mnem_transport::RemoteConfig::new(remote_name.clone(), file.url.clone());
cfg = cfg.with_token(token);
let client = HttpRemoteClient::new(cfg);
let local_head = repo
.view()
.heads
.first()
.cloned()
.ok_or_else(|| anyhow!("refusing to push: repository has no commits"))?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("building tokio runtime")?;
rt.block_on(async {
let refs_resp = client
.list_refs()
.await
.with_context(|| format!("list_refs against {}", file.url))?;
let remote_tip: Option<Cid> = refs_resp.refs.get(&branch).cloned();
if remote_tip.as_ref() == Some(&local_head) {
println!("Everything up-to-date");
return Ok::<(), anyhow::Error>(());
}
let mut car: Vec<u8> = Vec::new();
export(&*bs, &local_head, &mut car).context("export CAR from local blockstore")?;
let pushed = client
.push_blocks(bytes::Bytes::from(car))
.await
.map_err(map_client_err_push)?;
let _ = pushed;
let old = remote_tip.clone().unwrap_or_else(|| local_head.clone());
let cas = client
.advance_head(old.clone(), local_head.clone(), branch.clone())
.await;
match cas {
Ok(()) => {
let tracking_key = format!("refs/remotes/{remote_name}/{branch}");
let prev = repo.view().refs.get(&tracking_key).cloned();
let cfg_local = config::load(&data_dir)?;
let _ = repo.update_ref(
&tracking_key,
prev.as_ref(),
Some(RefTarget::normal(local_head.clone())),
&config::author_string(&cfg_local),
);
println!("To {}", file.url);
let old_short = remote_tip
.as_ref()
.map_or_else(|| "<new>".to_string(), short_cid);
println!(
" {old_short}..{} {branch} -> {remote_name}/{branch}",
short_cid(&local_head),
);
Ok(())
}
Err(ClientError::CasMismatch { .. }) => Err(anyhow!(
"Updates were rejected because tip of remote {branch} is ahead. \
Integrate remote changes (e.g. 'mnem pull') and try again."
)),
Err(ClientError::Auth(msg)) => {
let upper = remote_name.to_ascii_uppercase();
Err(anyhow!(
"Authentication required. Set MNEM_REMOTE_{upper}_TOKEN env var. ({msg})"
))
}
Err(e) => Err(anyhow!("advance_head failed: {e}")),
}
})
}
fn map_client_err_push(e: ClientError) -> anyhow::Error {
match e {
ClientError::Auth(msg) => {
anyhow!("Authentication required. Set MNEM_REMOTE_<NAME>_TOKEN env var. ({msg})")
}
other => anyhow!("push_blocks failed: {other}"),
}
}
fn resolve_token(remote_name: &str, token_env_hint: Option<&str>) -> Option<SecretToken> {
let upper = remote_name.to_ascii_uppercase();
let primary = format!("MNEM_REMOTE_{upper}_TOKEN");
if let Some(t) = SecretToken::from_env(&primary) {
return Some(t);
}
if let Some(var) = token_env_hint
&& let Some(t) = SecretToken::from_env(var)
{
return Some(t);
}
SecretToken::from_env("MNEM_HTTP_PUSH_TOKEN")
}
fn short_cid(c: &Cid) -> String {
let s = c.to_string();
let take = s.len().min(12);
s[..take].to_string()
}