OXYGENHUB

Coverage / crates/hub/src/repos/edits.rs 86.4%

51 of 59 lines run, 8 of 20 functions · run #471 on main · run missed

1
//! Changes the hub makes for a signed-in user (hub spec §6.9, §6.10, §7.6):
2
//! built on the target state, authored by the user's linked identity,
3
//! signed by the hub key as that identity's delegate, and applied through
4
//! the same checks as a push.
5
6
use super::{RepoError, Repos};
7
use crate::repos::RepoName;
8
use oxy_core::hash::{ChangeHash, KeyId};
9
use oxy_core::identity::{Domain, Identity, SecretKey};
10
use oxy_core::repo::{FileEdit, PushReport, Repo};
11
12
type Result<T> = std::result::Result<T, RepoError>;
13
140×
fn fail(msg: impl Into<String>) -> RepoError {
150×
    RepoError::Oxy(oxy_core::repo::RepoError::Msg(msg.into()))
160×
}
17
18
/// Who the change is for and who signs it.
19
pub struct Signer {
20
    /// The user's identity that lists the hub key as a delegate.
21
    pub author: Identity,
22
    pub hub_key: KeyId,
23
    pub hub_secret: SecretKey,
24
    pub hub_identity: Identity,
25
}
26
27
/// What a web edit did.
28
pub struct Committed {
29
    pub change: ChangeHash,
30
    pub bookmark: String,
31
    pub old_state: String,
32
    pub new_state: String,
33
    pub heads: Vec<ChangeHash>,
34
    pub report: PushReport,
35
}
36
37
/// Commit `edits` on top of `base_bookmark`'s state into `bookmark`
38
/// (the same bookmark, or a new one for a proposal).
3923×
pub async fn commit(
4023×
    repos: &Repos,
4123×
    repo: &RepoName,
4223×
    base_bookmark: String,
4323×
    bookmark: String,
4423×
    edits: Vec<FileEdit>,
4523×
    message: String,
4623×
    signer: Signer,
4723×
) -> Result<Committed> {
4823×
    repos
4923×
        .write(repo, move |r: &Repo, _| {
5023×
            let view = r.view()?;
5123×
            let base_heads = match view.bookmarks.get(&base_bookmark) {
5223×
                Some(st) => r.store().get_state(st)?.heads,
530×
                None => return Err(fail(format!("no bookmark {base_bookmark}"))),
54
            };
5523×
            if bookmark != base_bookmark && view.bookmarks.contains_key(&bookmark) {
560×
                return Err(fail(format!("bookmark {bookmark} exists; pick another name")));
5723×
            }
5823×
            let author = signer.author.hash();
5923×
            let change = r.build_change(&base_heads, &edits, &message, author)?;
6022×
            let hash = change.hash();
6122×
            let sig = signer.hub_secret.sign(Domain::Change, &signer.hub_key, &hash);
6222×
            let mut state = r.load_state(&base_heads)?;
6322×
            state.add(change.clone()).map_err(|e| fail(e.to_string()))?;
6422×
            let mut candidates = base_heads.clone();
6522×
            candidates.push(hash);
6622×
            let heads = state.maximal(&candidates);
6722×
            let update = r.host_ref_update(&bookmark, &heads, Some((&signer.hub_key, &signer.hub_secret)))?;
6822×
            let (old_state, new_state) = (update.old_state.to_hex(), update.new_state.to_hex());
6922×
            let report = r.apply_hosted(Some((change, vec![sig])), &[signer.hub_identity.clone(), signer.author.clone()], update)?;
7022×
            if let Some(refused) = report.results.iter().find(|x| !x.ok) {
710×
                return Err(fail(format!("the repository refused the change: {}", refused.reason)));
7222×
            }
7322×
            Ok(Committed { change: hash, bookmark, old_state, new_state, heads, report })
7423×
        })
7523×
        .await
7623×
}
77
78
/// Move `bookmark` to `heads` with an attestation signed by the hub key on
79
/// the user's behalf (landing a proposal under a policy).
808×
pub async fn move_bookmark(repos: &Repos, repo: &RepoName, bookmark: String, heads: Vec<ChangeHash>, signer: Signer) -> Result<PushReport> {
818×
    repos
828×
        .write(repo, move |r: &Repo, _| {
838×
            let update = r.host_ref_update(&bookmark, &heads, Some((&signer.hub_key, &signer.hub_secret)))?;
848×
            let report = r.apply_hosted(None, &[signer.hub_identity.clone(), signer.author.clone()], update)?;
858×
            if let Some(refused) = report.results.iter().find(|x| !x.ok) {
860×
                return Err(fail(format!("the repository refused the update: {}", refused.reason)));
878×
            }
888×
            Ok(report)
898×
        })
908×
        .await
918×
}