use mnem_core::id::Cid;
use mnem_core::objects::RefTarget;
use super::*;
#[derive(clap::Args, Debug)]
#[command(after_long_help = "\
Examples:
mnem pull # fast-forward origin/main into HEAD
mnem pull origin main
Fast-forward only. Use `mnem merge <remote>/<branch>` (B4) for 3-way merges.
")]
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();
crate::commands::fetch::run(
override_path,
crate::commands::fetch::Args {
remote: Some(remote_name.clone()),
},
)?;
let (data_dir, repo, bs, _ohs) = repo::open_all(override_path)?;
let tracking_key = format!("refs/remotes/{remote_name}/{branch}");
let remote_tip = match repo.view().refs.get(&tracking_key) {
Some(RefTarget::Normal { target }) => target.clone(),
_ => {
return Err(anyhow!(
"no tracking ref {tracking_key}; did `mnem fetch {remote_name}` complete?"
));
}
};
let local_head = repo.view().heads.first().cloned();
let local_branch_key = format!("refs/heads/{branch}");
let local_branch = repo.view().refs.get(&local_branch_key).cloned();
if let Some(lh) = &local_head
&& lh == &remote_tip
{
println!("Already up to date.");
return Ok(());
}
if let Some(lh) = &local_head
&& !is_ancestor(&*bs, lh, &remote_tip)?
{
return Err(anyhow!(
"mnem pull: non-fast-forward detected, use 'mnem merge {remote_name}/{branch}' \
(requires B4 merge verb)"
));
}
let cfg_local = config::load(&data_dir)?;
let author = config::author_string(&cfg_local);
let after_ref = repo
.update_ref(
&local_branch_key,
local_branch.as_ref(),
Some(RefTarget::normal(remote_tip.clone())),
&author,
)
.with_context(|| format!("update_ref {local_branch_key}"))?;
after_ref
.update_heads(remote_tip.clone(), &author)
.context("advancing HEAD after pull")?;
println!(
"Fast-forward {} -> {}",
local_head
.as_ref()
.map_or_else(|| "<empty>".to_string(), short_cid),
short_cid(&remote_tip),
);
Ok(())
}
fn is_ancestor(bs: &dyn mnem_core::store::Blockstore, needle: &Cid, tip: &Cid) -> Result<bool> {
use mnem_core::codec::from_canonical_bytes;
use std::collections::{HashSet, VecDeque};
let mut seen: HashSet<Cid> = HashSet::new();
let mut q: VecDeque<Cid> = VecDeque::new();
q.push_back(tip.clone());
while let Some(cur) = q.pop_front() {
if !seen.insert(cur.clone()) {
continue;
}
if &cur == needle {
return Ok(true);
}
let Some(bytes) = bs.get(&cur)? else {
continue;
};
let Ok(commit) = from_canonical_bytes::<Commit>(&bytes) else {
continue;
};
for p in &commit.parents {
q.push_back(p.clone());
}
if seen.len() > 10_000 {
break;
}
}
Ok(false)
}
fn short_cid(c: &Cid) -> String {
let s = c.to_string();
let take = s.len().min(12);
s[..take].to_string()
}