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 1 month ago · 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
crates/hub/tests/browse.rs templates/overview.html templates/style.css
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("<></span> Code</summary>"), "a Code button");
136
+
assert!(o.contains("href=\"/andy/demo/archive?ref=main&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"><></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 }}&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 }}&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