//! Changes the hub makes for a signed-in user (hub spec §6.9, §6.10, §7.6): //! built on the target state, authored by the user's linked identity, //! signed by the hub key as that identity's delegate, and applied through //! the same checks as a push. use super::{RepoError, Repos}; use crate::repos::RepoName; use oxy_core::hash::{ChangeHash, KeyId}; use oxy_core::identity::{Domain, Identity, SecretKey}; use oxy_core::repo::{FileEdit, PushReport, Repo}; type Result = std::result::Result; fn fail(msg: impl Into) -> RepoError { RepoError::Oxy(oxy_core::repo::RepoError::Msg(msg.into())) } /// Who the change is for and who signs it. pub struct Signer { /// The user's identity that lists the hub key as a delegate. pub author: Identity, pub hub_key: KeyId, pub hub_secret: SecretKey, pub hub_identity: Identity, } /// What a web edit did. pub struct Committed { pub change: ChangeHash, pub bookmark: String, pub old_state: String, pub new_state: String, pub heads: Vec, pub report: PushReport, } /// Commit `edits` on top of `base_bookmark`'s state into `bookmark` /// (the same bookmark, or a new one for a proposal). pub async fn commit( repos: &Repos, repo: &RepoName, base_bookmark: String, bookmark: String, edits: Vec, message: String, signer: Signer, ) -> Result { repos .write(repo, move |r: &Repo, _| { let view = r.view()?; let base_heads = match view.bookmarks.get(&base_bookmark) { Some(st) => r.store().get_state(st)?.heads, None => return Err(fail(format!("no bookmark {base_bookmark}"))), }; if bookmark != base_bookmark && view.bookmarks.contains_key(&bookmark) { return Err(fail(format!("bookmark {bookmark} exists; pick another name"))); } let author = signer.author.hash(); let change = r.build_change(&base_heads, &edits, &message, author)?; let hash = change.hash(); let sig = signer.hub_secret.sign(Domain::Change, &signer.hub_key, &hash); let mut state = r.load_state(&base_heads)?; state.add(change.clone()).map_err(|e| fail(e.to_string()))?; let mut candidates = base_heads.clone(); candidates.push(hash); let heads = state.maximal(&candidates); let update = r.host_ref_update(&bookmark, &heads, Some((&signer.hub_key, &signer.hub_secret)))?; let (old_state, new_state) = (update.old_state.to_hex(), update.new_state.to_hex()); let report = r.apply_hosted(Some((change, vec![sig])), &[signer.hub_identity.clone(), signer.author.clone()], update)?; if let Some(refused) = report.results.iter().find(|x| !x.ok) { return Err(fail(format!("the repository refused the change: {}", refused.reason))); } Ok(Committed { change: hash, bookmark, old_state, new_state, heads, report }) }) .await } /// Move `bookmark` to `heads` with an attestation signed by the hub key on /// the user's behalf (landing a proposal under a policy). pub async fn move_bookmark(repos: &Repos, repo: &RepoName, bookmark: String, heads: Vec, signer: Signer) -> Result { repos .write(repo, move |r: &Repo, _| { let update = r.host_ref_update(&bookmark, &heads, Some((&signer.hub_key, &signer.hub_secret)))?; let report = r.apply_hosted(None, &[signer.hub_identity.clone(), signer.author.clone()], update)?; if let Some(refused) = report.results.iter().find(|x| !x.ok) { return Err(fail(format!("the repository refused the update: {}", refused.reason))); } Ok(report) }) .await }