OXYGENHUB

Push log records states, heads and change counts from the push report 62d6e969f180

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-27 17:56 UTC
Signature
✔ signed by Andy McCall
Hash
62d6e969f180b5b0d84205d80f1082b820680e4449d30cabac3aee21a819e17b
Depends on
d23464c0ac60
Depended on by
0ee5c3fb32eb 5d90e0e045e2 2a0c09c09716 4bae285c6ed8 941abdd6bb4c 78001759604c 18e381ea88c2 19bb31ff1f5c e1078e99c560
In bookmarks
api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
In tags
v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
17 atoms, 1 tree operation, 6 files

Browse the tree at this change

added crates/hub/migrations/0002_push_details.sql +5 −0
1 +
-- What a push moved: the states and heads (hub spec §6.12).
2 +
ALTER TABLE pushes ADD COLUMN old_state TEXT NOT NULL DEFAULT '';
3 +
ALTER TABLE pushes ADD COLUMN new_state TEXT NOT NULL DEFAULT '';
4 +
ALTER TABLE pushes ADD COLUMN heads INTEGER NOT NULL DEFAULT 0;
5 +
ALTER TABLE pushes ADD COLUMN changes INTEGER NOT NULL DEFAULT 0;
modified crates/hub/src/db/repos.rs +28 −8
…
32 unchanged lines
33 33
    pub reference: String,
34 34
    pub ok: bool,
35 35
    pub reason: String,
36 +
    pub old_state: String,
37 +
    pub new_state: String,
38 +
    pub heads: i64,
39 +
    pub changes: i64,
40 +
}
41 +
42 +
/// One reference's outcome in a push, as the protocol backend reports it.
43 +
#[derive(Clone, Debug)]
44 +
pub struct PushOutcome {
45 +
    pub reference: String,
46 +
    pub ok: bool,
47 +
    pub reason: String,
48 +
    pub old_state: String,
49 +
    pub new_state: String,
50 +
    pub heads: i64,
51 +
    pub changes: i64,
36 52
}
37 53
38 54
pub async fn list(pool: &Pool) -> sqlx::Result<Vec<RepoRow>> {
…
44 unchanged lines
83 99
}
84 100
85 101
/// Record a push's per-reference results and bump the repository's activity.
86 -
pub async fn record_push(pool: &Pool, id: i64, results: &[(String, bool, String)]) -> sqlx::Result<()> {
102 +
pub async fn record_push(pool: &Pool, id: i64, results: &[PushOutcome]) -> sqlx::Result<()> {
87 103
    let now = now_ms();
88 104
    let mut tx = pool.begin().await?;
89 -
    for (reference, ok, reason) in results {
90 -
        sqlx::query("INSERT INTO pushes (repo_id, at_ms, reference, ok, reason) VALUES (?, ?, ?, ?, ?)")
105 +
    for r in results {
106 +
        sqlx::query("INSERT INTO pushes (repo_id, at_ms, reference, ok, reason, old_state, new_state, heads, changes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
91 107
            .bind(id)
92 108
            .bind(now)
93 -
            .bind(reference)
94 -
            .bind(ok)
95 -
            .bind(reason)
109 +
            .bind(&r.reference)
110 +
            .bind(r.ok)
111 +
            .bind(&r.reason)
112 +
            .bind(&r.old_state)
113 +
            .bind(&r.new_state)
114 +
            .bind(r.heads)
115 +
            .bind(r.changes)
96 116
            .execute(&mut *tx)
97 117
            .await?;
98 118
    }
99 -
    if results.iter().any(|(_, ok, _)| *ok) {
119 +
    if results.iter().any(|r| r.ok) {
100 120
        sqlx::query("UPDATE repositories SET updated_ms = ? WHERE id = ?").bind(now).bind(id).execute(&mut *tx).await?;
101 121
    }
102 122
    tx.commit().await
103 123
}
104 124
105 125
pub async fn pushes(pool: &Pool, id: i64, limit: i64) -> sqlx::Result<Vec<PushRow>> {
106 -
    sqlx::query_as("SELECT id, at_ms, reference, ok, reason FROM pushes WHERE repo_id = ? ORDER BY at_ms DESC, id DESC LIMIT ?")
126 +
    sqlx::query_as("SELECT id, at_ms, reference, ok, reason, old_state, new_state, heads, changes FROM pushes WHERE repo_id = ? ORDER BY at_ms DESC, id DESC LIMIT ?")
107 127
        .bind(id)
108 128
        .bind(limit)
109 129
        .fetch_all(pool)
…
2 unchanged lines
modified crates/hub/src/protocol.rs +18 −4
…
8 unchanged lines
9 9
use axum::http::{HeaderMap, Method, StatusCode};
10 10
use axum::response::{IntoResponse, Response};
11 11
use hub_api::Visibility;
12 -
use oxy_core::protocol::RefResult;
13 -
use oxy_core::repo::Repo;
12 +
use oxy_core::repo::{PushReport, Repo};
14 13
use oxy_server::{Access, Backend, Endpoint, Failure};
15 14
use std::sync::Arc;
16 15
…
57 unchanged lines
74 73
        }
75 74
    }
76 75
77 -
    fn after_push(&self, name: &str, results: &[RefResult]) {
76 +
    fn after_push(&self, name: &str, report: &PushReport) {
78 77
        let Some(parsed) = RepoName::parse(name) else { return };
79 78
        let Ok(row) = self.row(&parsed) else { return };
80 79
        let pool = self.pool.clone();
81 -
        let results: Vec<(String, bool, String)> = results.iter().map(|r| (r.name.clone(), r.ok, r.reason.clone())).collect();
80 +
        let results: Vec<db::repos::PushOutcome> = report
81 +
            .results
82 +
            .iter()
83 +
            .map(|r| {
84 +
                let updated = report.updated.iter().find(|u| u.name == r.name);
85 +
                db::repos::PushOutcome {
86 +
                    reference: r.name.clone(),
87 +
                    ok: r.ok,
88 +
                    reason: r.reason.clone(),
89 +
                    old_state: updated.map(|u| u.old_state.to_hex()).unwrap_or_default(),
90 +
                    new_state: updated.map(|u| u.new_state.to_hex()).unwrap_or_default(),
91 +
                    heads: updated.map(|u| u.heads.len() as i64).unwrap_or(0),
92 +
                    changes: if r.ok { report.imported.len() as i64 } else { 0 },
93 +
                }
94 +
            })
95 +
            .collect();
82 96
        self.runtime.spawn(async move {
83 97
            if let Err(e) = db::repos::record_push(&pool, row.id, &results).await {
84 98
                tracing::warn!("recording push to {}: {e}", parsed);
…
31 unchanged lines
modified crates/hub/tests/browse.rs +1 −0
…
169 unchanged lines
170 170
    assert!(c.contains("signed by Test Author"), "change page shows the signer");
171 171
    let pushes = page(&h.app, "/andy/demo/pushes").await;
172 172
    assert!(pushes.contains("accepted") && pushes.contains("main"), "{pushes}");
173 +
    assert!(pushes.contains("<td>2</td>") && pushes.contains("<code title="), "two changes imported and the state shown: {pushes}");
173 174
174 175
    // API and small endpoints.
175 176
    let (_, repos) = get(&h.app, "/api/v1/repos").await;
…
46 unchanged lines
modified docs/plan.md +2 −0
…
27 unchanged lines
28 28
29 29
- State caching (spec §16.3) is not started: pages load the state they need per request; the overview loads it once and reuses it. Measured on the `oxygen` repository (88 changes): overview ~0.5 s, tree ~0.14 s, history ~0.13 s, a 2 000-line file ~0.7 s. A per-heads cache is the first performance item for 0.2.
30 30
- `State` holds `Rc` values, so a cross-thread cache needs a `Send` snapshot type in `oxy-core` first.
31 +
32 +
The cross-product order of work is in `oxygen/docs/roadmap.md`.
modified templates/pushes.html +2 −2
…
6 unchanged lines
7 7
<div class="head"><h1>Pushes <small>every reference update the hub accepted or refused</small></h1></div>
8 8
{% if pushes.is_empty() %}<p class="empty">Nothing has been pushed yet.</p>{% else %}
9 9
<div class="table"><table>
10 -
<tr><th>When</th><th>Reference</th><th>Result</th></tr>
10 +
<tr><th>When</th><th>Reference</th><th>Result</th><th>Changes</th><th>Heads</th><th>State</th></tr>
11 11
{% for p in pushes %}
12 -
<tr><td><time title="{{ p.at_ms|when_i }}">{{ p.at_ms|ago_i }}</time></td><td class="px">{{ p.reference }}</td><td>{% if p.ok %}<span class="sig sig-valid">✔</span> accepted{% else %}<span class="sig sig-invalid">✘</span> refused: {{ p.reason }}{% endif %}</td></tr>
12 +
<tr><td><time title="{{ p.at_ms|when_i }}">{{ p.at_ms|ago_i }}</time></td><td class="px">{{ p.reference }}</td><td>{% if p.ok %}<span class="sig sig-valid">✔</span> accepted{% else %}<span class="sig sig-invalid">✘</span> refused: {{ p.reason }}{% endif %}</td><td>{% if p.ok %}{{ p.changes }}{% endif %}</td><td>{% if p.ok %}{{ p.heads }}{% endif %}</td><td class="small mute">{% if !p.new_state.is_empty() %}<code title="{{ p.old_state }} → {{ p.new_state }}">{{ p.new_state|short }}</code>{% endif %}</td></tr>
13 13
{% endfor %}
14 14
</table></div>
15 15
{% endif %}
…
1 unchanged lines