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 |