OXYGENHUB
# a268d4df52bf oxygenhub / crates / hub / src / repos / edits.rs
3.8 KiB · Rust · last changed Web edits and browser conflict resolution; landing under a policy 1 month ago source blame history raw
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
14
fn fail(msg: impl Into<String>) -> RepoError {
15
    RepoError::Oxy(oxy_core::repo::RepoError::Msg(msg.into()))
16
}
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).
39
pub async fn commit(
40
    repos: &Repos,
41
    repo: &RepoName,
42
    base_bookmark: String,
43
    bookmark: String,
44
    edits: Vec<FileEdit>,
45
    message: String,
46
    signer: Signer,
47
) -> Result<Committed> {
48
    repos
49
        .write(repo, move |r: &Repo, _| {
50
            let view = r.view()?;
51
            let base_heads = match view.bookmarks.get(&base_bookmark) {
52
                Some(st) => r.store().get_state(st)?.heads,
53
                None => return Err(fail(format!("no bookmark {base_bookmark}"))),
54
            };
55
            if bookmark != base_bookmark && view.bookmarks.contains_key(&bookmark) {
56
                return Err(fail(format!("bookmark {bookmark} exists; pick another name")));
57
            }
58
            let author = signer.author.hash();
59
            let change = r.build_change(&base_heads, &edits, &message, author)?;
60
            let hash = change.hash();
61
            let sig = signer.hub_secret.sign(Domain::Change, &signer.hub_key, &hash);
62
            let mut state = r.load_state(&base_heads)?;
63
            state.add(change.clone()).map_err(|e| fail(e.to_string()))?;
64
            let mut candidates = base_heads.clone();
65
            candidates.push(hash);
66
            let heads = state.maximal(&candidates);
67
            let update = r.host_ref_update(&bookmark, &heads, Some((&signer.hub_key, &signer.hub_secret)))?;
68
            let (old_state, new_state) = (update.old_state.to_hex(), update.new_state.to_hex());
69
            let report = r.apply_hosted(Some((change, vec![sig])), &[signer.hub_identity.clone(), signer.author.clone()], update)?;
70
            if let Some(refused) = report.results.iter().find(|x| !x.ok) {
71
                return Err(fail(format!("the repository refused the change: {}", refused.reason)));
72
            }
73
            Ok(Committed { change: hash, bookmark, old_state, new_state, heads, report })
74
        })
75
        .await
76
}
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).
80
pub async fn move_bookmark(repos: &Repos, repo: &RepoName, bookmark: String, heads: Vec<ChangeHash>, signer: Signer) -> Result<PushReport> {
81
    repos
82
        .write(repo, move |r: &Repo, _| {
83
            let update = r.host_ref_update(&bookmark, &heads, Some((&signer.hub_key, &signer.hub_secret)))?;
84
            let report = r.apply_hosted(None, &[signer.hub_identity.clone(), signer.author.clone()], update)?;
85
            if let Some(refused) = report.results.iter().find(|x| !x.ok) {
86
                return Err(fail(format!("the repository refused the update: {}", refused.reason)));
87
            }
88
            Ok(report)
89
        })
90
        .await
91
}