OXYGENHUB

A Code button with a zip download 67b24adbf35a

The repository page's Clone button is now a <> Code button: the oxy
clone command as before, and under it Download ZIP of the default
bookmark as it is now (the source archive, built once per state), with
.tar.zst beside it.

closes #61
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 11:16 UTC
Signature
✔ signed by Andy McCall
Hash
67b24adbf35ad837aba115aadba8fac7993f2ccb4e7362d60505d2cc28b7cbee
Depends on
6ad368c5958e 83d5052bf76c d23464c0ac60
Depended on by
7b17d570d7ee ffb67a81188c 7f283f8e8b97
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
5 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/hub/tests/browse.rs +6 −0
…
130 unchanged lines
131 131
    assert!(o.contains("2 changes"));
132 132
    // Overview: clone popover, contributors, languages, visibility badge.
133 133
    assert!(o.contains("class=\"clone-pop\"") && o.contains("value=\"oxy clone "), "clone behind a button");
134 +
    // The Code button also offers the default bookmark as a zip.
135 +
    assert!(o.contains("&lt;&gt;</span> Code</summary>"), "a Code button");
136 +
    assert!(o.contains("href=\"/andy/demo/archive?ref=main&amp;format=zip\" download>"), "a zip download: {o}");
137 +
    let (status, zip) = get(&h.app, "/andy/demo/archive?ref=main&format=zip").await;
138 +
    assert_eq!(status, StatusCode::OK);
139 +
    assert!(zip.starts_with("PK"), "a zip archive");
134 140
    assert!(o.contains("<h3>Contributors") && o.contains("<h3>Languages</h3>") && o.contains("<strong>Rust</strong>"), "{o}");
135 141
    assert!(o.contains("class=\"vis vis-public\""));
136 142
    // Landmarks, skip link, current tab (hub spec §6.13).
…
148 unchanged lines
modified templates/overview.html +6 −2
…
13 unchanged lines
14 14
      {% if repo.can(crate::auth::Access::Write) %}<a class="go small" href="{{ repo.url }}/newfile/{{ repo.default_bookmark }}">new file</a>{% endif %}
15 15
      <form class="find inline-find" method="get" action="{{ repo.url }}/search"><input type="search" name="q" placeholder="Search code…"></form>
16 16
      <details class="clone-pop">
17 -
        <summary class="go small" aria-haspopup="true">⤓ Clone</summary>
18 -
        <div class="clone-panel" role="group" aria-label="Clone this repository">
17 +
        <summary class="go small" aria-haspopup="true"><span aria-hidden="true">&lt;&gt;</span> Code</summary>
18 +
        <div class="clone-panel" role="group" aria-label="Clone or download this repository">
19 19
          <label for="clone-cmd" class="small mute">Clone with oxy</label>
20 20
          <div class="clone"><input id="clone-cmd" type="text" readonly value="oxy clone {{ repo.clone_url }}" onfocus="this.select()" onclick="this.select()"><button type="button" class="small" data-copy="oxy clone {{ repo.clone_url }}">copy</button></div>
21 21
          <p class="small mute">The URL alone: <code>{{ repo.clone_url }}</code></p>
22 +
          {% if quickstart.is_none() %}
23 +
          <a class="download" href="{{ repo.url }}/archive?ref={{ repo.default_bookmark|urlencode }}&amp;format=zip" download><span aria-hidden="true">⤓</span> Download ZIP</a>
24 +
          <p class="small mute">{{ repo.default_bookmark }} as it is now, without its history. Also as <a href="{{ repo.url }}/archive?ref={{ repo.default_bookmark|urlencode }}&amp;format=tar.zst" download>.tar.zst</a>.</p>
25 +
          {% endif %}
22 26
        </div>
23 27
      </details>
24 28
    </div>
…
50 unchanged lines
modified templates/style.css +2 −0
…
526 unchanged lines
527 527
.clone-panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 20; width: min(440px, 88vw); background: var(--panel); border: 1px solid var(--line); box-shadow: 0 6px 24px rgba(0,0,0,.25); padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
528 528
.clone-panel .clone input { flex: 1; min-width: 0; margin: 0; font-family: var(--mono, monospace); font-size: 12px; }
529 529
.clone-panel p { margin: 0; overflow-wrap: anywhere; }
530 +
.clone-panel .download { border-top: 1px solid var(--line); padding-top: 8px; margin-top: 2px; font-weight: 600; text-decoration: none; }
531 +
.clone-panel .download:hover { text-decoration: underline; }
530 532
.bar .inline-find { flex: 1 1 200px; }
531 533
ul.contributors li { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 4px 0; }
532 534
ul.contributors img.avatar.sm { width: 20px; height: 20px; vertical-align: middle; }
…
13 unchanged lines