crates/cli/git-ents/src/commands/inbox.rs
inbox.rshistorycomment on this file
| 1 | //! `git ents inbox`: list entities awaiting adoption and adopt them onto |
| 2 | //! their canonical ref (`sync.adoption-machinery`, |
| 3 | //! `sync.adoption-no-cherry-pick`). |
| 4 | |
| 5 | use ents_model::namespace; |
| 6 | use ents_sync::resolve::{Heads, Merged, merge_heads}; |
| 7 | use gix_ref_store::RefStoreRead; |
| 8 | |
| 9 | use super::{actor, signer}; |
| 10 | use crate::error::{Error, Result}; |
| 11 | use crate::mutate::outcome_to_result; |
| 12 | use crate::root::LocalRoot; |
| 13 | |
| 14 | /// `git ents inbox list`: every `refs/meta/inbox/<member>/<id>` entry. |
| 15 | /// |
| 16 | /// # Errors |
| 17 | /// |
| 18 | /// Propagates a ref-store read failure. |
| 19 | pub fn list(root: &LocalRoot) -> Result<Vec<String>> { |
| 20 | let mut out = Vec::new(); |
| 21 | for entry in root.refs.iter_prefix("refs/meta/inbox/")? { |
| 22 | let (name, _) = entry?; |
| 23 | let path = name.as_bstr().to_string(); |
| 24 | if let Some(rest) = path.strip_prefix("refs/meta/inbox/") { |
| 25 | out.push(rest.to_owned()); |
| 26 | } |
| 27 | } |
| 28 | Ok(out) |
| 29 | } |
| 30 | |
| 31 | /// `git ents inbox adopt`: fold `entry` (`<member>/<id>`) onto its |
| 32 | /// canonical ref (`refs/meta/<id>`) via [`merge_heads`], keeping the |
| 33 | /// author's original signed commit in ancestry |
| 34 | /// (`sync.adoption-no-cherry-pick`). |
| 35 | /// |
| 36 | /// # Errors |
| 37 | /// |
| 38 | /// [`Error::NotFound`] if `entry` has no inbox ref; [`Error::InvalidArgument`] |
| 39 | /// on a merge conflict (a human must resolve it before adoption can |
| 40 | /// complete — this phase does not implement interactive conflict |
| 41 | /// resolution); otherwise see [`crate::mutate::outcome_to_result`]. |
| 42 | pub fn adopt(root: &LocalRoot, entry: &str, key: Option<std::path::PathBuf>) -> Result<()> { |
| 43 | let Some((member, id)) = entry.split_once('/') else { |
| 44 | return Err(Error::InvalidArgument(format!( |
| 45 | "expected <member>/<id>, got {entry:?}" |
| 46 | ))); |
| 47 | }; |
| 48 | let inbox_ref = namespace::inbox_ref(&ents_model::MemberId::new(member), id)?; |
| 49 | let Some(theirs) = root.refs.get(inbox_ref.as_ref())? else { |
| 50 | return Err(Error::NotFound { |
| 51 | what: format!("inbox entry {entry}"), |
| 52 | }); |
| 53 | }; |
| 54 | let canonical: gix::refs::FullName = format!("refs/meta/{id}") |
| 55 | .try_into() |
| 56 | .map_err(|_source| Error::InvalidArgument(format!("bad canonical ref for {id}")))?; |
| 57 | let ours = root.refs.get(canonical.as_ref())?; |
| 58 | |
| 59 | let signer = signer(root, key)?; |
| 60 | let author = actor(&signer); |
| 61 | let heads = Heads { |
| 62 | refname: canonical.clone(), |
| 63 | ours, |
| 64 | theirs, |
| 65 | }; |
| 66 | let merged = merge_heads( |
| 67 | &root.objects, |
| 68 | &heads, |
| 69 | &author, |
| 70 | &format!("Adopt {entry}"), |
| 71 | |payload| signer.sign(payload), |
| 72 | )?; |
| 73 | let tip = match merged { |
| 74 | Merged::Tip(tip) => tip, |
| 75 | Merged::Conflict(paths) => { |
| 76 | let rendered = paths |
| 77 | .iter() |
| 78 | .map(|p| p.to_string()) |
| 79 | .collect::<Vec<_>>() |
| 80 | .join(", "); |
| 81 | return Err(Error::InvalidArgument(format!( |
| 82 | "adoption conflict at: {rendered}" |
| 83 | ))); |
| 84 | } |
| 85 | }; |
| 86 | |
| 87 | let proposal = ents_receive::Proposal { |
| 88 | transitions: vec![ents_receive::RefTransition { |
| 89 | name: canonical, |
| 90 | old: ours, |
| 91 | new: Some(tip), |
| 92 | }], |
| 93 | objects: vec![tip], |
| 94 | auth: None, |
| 95 | }; |
| 96 | let outcome = ents_receive::receive( |
| 97 | &root.refs, |
| 98 | &root.objects, |
| 99 | &root.events, |
| 100 | &proposal, |
| 101 | root.mode(), |
| 102 | )?; |
| 103 | outcome_to_result(outcome, ours)?; |
| 104 | Ok(()) |
| 105 | } |