lmjtfy.git / apps / lmjtfy / src / clone.rs
clone.rsannotatedclone.rssource566 lines · 23.4 KB · raw

git clone https://lmjtfy.fun/lmjtfy.git: the code, for anyone, without a GitHub account and without publishing it on GitHub.

Git's smart HTTP is two requests: GET <repo>/info/refs?service= git-upload-pack lists the refs, and POST <repo>/git-upload-pack sends the pack. Both are forwarded to the private repository on GitHub with a fine-grained token that can read these two repositories and do nothing else, so a push through here is refused by GitHub, not only by the routes. Nothing is kept and nothing is spent: a clone reaches neither Jev nor the LLM, and the budgets do not count it.

jevcrates is served beside lmjtfy because lmjtfy's .gitmodules names it by a relative URL (../jevcrates.git), which git resolves against wherever the clone came from: here, the Worker; from GitHub, GitHub.

16use axum::body::{Body, to_bytes};
17use axum::extract::{Path, Query, State};
18use axum::http::{HeaderMap, Method, Response, StatusCode, header};
19use base64::Engine;
20use base64::engine::general_purpose::STANDARD;
21use serde::Deserialize;
23use std::cell::RefCell;
24
25use archive::Fetch;
26use card::Commit;
27use std::io::Read;
28
29use futures_util::StreamExt;
30
31use super::App;

The secret's name: a GitHub fine-grained token with Contents: read on deizel/lmjtfy and deizel/jevcrates, and no other permission.

35const TOKEN: &str = "LMJTFY_GITHUB_TOKEN";

The one git service served. git-receive-pack is a push.

38const UPLOAD_PACK: &str = "git-upload-pack";

The largest request body forwarded. An upload-pack request is the refs a client wants and has, a few kilobytes even for a long history.

42const MAX_REQUEST_BYTES: usize = 1 << 20;

The headers a git client sends that change GitHub's answer, forwarded as they came. git-protocol selects protocol v2, which modern git asks for; GitHub answers smart HTTP only to a git/ user agent.

47const FORWARDED: [&str; 5] = ["accept", "content-type", "content-encoding", "git-protocol", "user-agent"];

The latest commit on lmjtfy's main, from GitHub's API. The token's Contents: read covers it. per_page=1 makes the last page's number the count of commits.

How long an isolate keeps the latest commit before asking again. Chats fetch a pasted link's page and picture together; this makes that one call, not two.

56const LATEST_MS: f64 = 60_000.0;
58thread_local! {

Each repository's latest commit and when it was read, in this isolate.

60    static LATEST: RefCell<Vec<(Repo, f64, Commit)>> = const { RefCell::new(Vec::new()) };
61}

The repositories that can be cloned from here. Anything else is not found. lmjtfy and the three other projects that ask Jev, jevcrates, the client those four share, and jevstrudel, which asks Jev from the browser.

66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum Repo {
68    Lmjtfy,
69    Jevcrates,
70    Postjevsql,
71    Jevsnes,
72    Jevhooks,
73    Jevstrudel,
74}
76impl Repo {

In the order the code pages list them: this site, the client, then the other projects that use it.

79    pub const ALL: [Repo; 6] = [Repo::Lmjtfy, Repo::Jevcrates, Repo::Postjevsql, Repo::Jevsnes, Repo::Jevhooks, Repo::Jevstrudel];

The path segment it is cloned by: lmjtfy.git.

82    pub fn served(self) -> &'static str {
83        match self {
84            Repo::Lmjtfy => "lmjtfy.git",
85            Repo::Jevcrates => "jevcrates.git",
86            Repo::Postjevsql => "postjevsql.git",
87            Repo::Jevsnes => "jevsnes.git",
88            Repo::Jevhooks => "jevhooks.git",
89            Repo::Jevstrudel => "jevstrudel.git",
90        }
91    }
93    pub fn named(segment: &str) -> Option<Repo> {
94        Repo::ALL.into_iter().find(|repo| repo.served() == segment)
95    }

owner/name on GitHub.

98    pub fn github(self) -> &'static str {
99        match self {
100            Repo::Lmjtfy => "deizel/lmjtfy",
101            Repo::Jevcrates => "deizel/jevcrates",
102            Repo::Postjevsql => "deizel/postjevsql",
103            Repo::Jevsnes => "deizel/jevsnes",
104            Repo::Jevhooks => "deizel/jevhooks",
105            Repo::Jevstrudel => "deizel/jevstrudel",
106        }
107    }

What it is, in a few words, for the list of repositories.

110    pub fn blurb(self) -> &'static str {
111        match self {
112            Repo::Lmjtfy => "This site: Rust on Cloudflare Workers, a rules engine, and Jev.",
113            Repo::Jevcrates => "The Rust client for Jev that all four projects share.",
114            Repo::Postjevsql => "Jev in Postgres: a pgrx extension, in SQL.",
115            Repo::Jevsnes => "Jev plays A Link to the Past on a SNES core.",
116            Repo::Jevhooks => "Jev judges Claude Code's hooks.",
117            Repo::Jevstrudel => "Jev arranges songs live, in a fork of Strudel.",
118        }
119    }

Its submodules: where each is mounted, and which repository it is. The test below holds this to .gitmodules, where the checkout has it.

123    pub fn submodules(self) -> &'static [(&'static str, Repo)] {
124        match self {
125            Repo::Lmjtfy | Repo::Postjevsql | Repo::Jevhooks => &[("third-party/jevcrates", Repo::Jevcrates)],
126            Repo::Jevsnes => &[("third-party/rust/jevcrates", Repo::Jevcrates)],
127            Repo::Jevcrates | Repo::Jevstrudel => &[],
128        }
129    }

The branch the pages read: GitHub's default for it.

132    pub fn branch(self) -> &'static str {
133        match self {
134            Repo::Jevstrudel => "jevstrudel",
135            _ => "main",
136        }
137    }
139    fn upstream(self) -> String {
140        format!("https://github.com/{}.git", self.github())
141    }

The command the page offers, for this site's origin.

144    pub fn command(self, origin: &str) -> String {
145        let recurse = if self.submodules().is_empty() { "" } else { "--recurse-submodules " };
146        format!("git clone {recurse}{origin}/{}", self.served())
147    }

What the code pages need to know about it.

150    pub fn project(self) -> crate::view::code::Project {
151        crate::view::code::Project { served: self.served(), blurb: self.blurb(), branch: self.branch() }
152    }
153}

The command the home page offers: lmjtfy's.

156pub fn command(origin: &str) -> String {
157    Repo::Lmjtfy.command(origin)
158}

?view=agents on the front page shows the root CLAUDE.md.

161#[derive(Deserialize)]
162pub struct Viewed {
163    view: Option<String>,
164}
166#[derive(Deserialize)]
167pub struct Service {
168    #[serde(default)]
169    service: String,
170}

GET /lmjtfy.git (or any served repository) from a browser or a chat's link preview: its front page. Git never asks for this path, so the clone address is also the page.

175#[worker::send]
176pub async fn landing(
177    State(app): State<App>,
178    Path(segment): Path<String>,
179    Query(asked): Query<Viewed>,
180    headers: HeaderMap,
181) -> Response<Body> {
182    let Some(repo) = Repo::named(&segment) else { return refused(StatusCode::NOT_FOUND, "No such page.") };
183    let latest = latest(&app.env, repo).await;
184    let view = crate::view::code::View::asked(asked.view.as_deref());
185    let docs = crate::browse::root(&app.env, repo, view).await;
186    let whole = crate::browse::explorer(&app.env, repo).await;
187    let origin = super::origin(&headers);
188    let frame = crate::browse::frame(&origin, repo, "", &whole, latest.as_ref());
189    let page = crate::view::code::page(&frame, crate::view::code::Shown::Dir { docs });
190    Response::builder()
191        .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
192        .header(header::CACHE_CONTROL, "no-cache")
193        .body(Body::from(page.into_string()))
194        .expect("static headers are valid")
195}

The latest commit on main, read at most once a minute per isolate. If GitHub cannot be read, the last one read, however old; None only if there has never been one.

200pub async fn latest(env: &worker::Env, repo: Repo) -> Option<Commit> {
201    let now = js_sys::Date::now();
202    let kept = LATEST.with(|latest| latest.borrow().iter().find(|(seen, _, _)| *seen == repo).map(|(_, at, commit)| (*at, commit.clone())));
203    if let Some((at, commit)) = &kept
204        && now - at < LATEST_MS
205    {
206        return Some(commit.clone());
207    }
208    match read_latest(env, repo).await {
209        Some(commit) => {
210            LATEST.with(|latest| {
211                let mut latest = latest.borrow_mut();
212                latest.retain(|(seen, _, _)| *seen != repo);
213                latest.push((repo, now, commit.clone()));
214            });
215            Some(commit)
216        }
217        None => kept.map(|(_, commit)| commit),
218    }
219}
221async fn read_latest(env: &worker::Env, repo: Repo) -> Option<Commit> {
222    let url = format!("https://api.github.com/repos/{}/commits?sha={}&per_page=1", repo.github(), repo.branch());
223    match github(env, &url).await? {
224        (200, body, link) => listed(&body, link.as_deref()),
225        _ => None,
226    }
227}

A GET of GitHub's API with the read-only token: its status, body and link header. None if there is no token or GitHub did not answer.

231pub async fn github(env: &worker::Env, url: &str) -> Option<(u16, String, Option<String>)> {
232    let token = env.secret(TOKEN).ok()?.to_string();
233    let headers = worker::Headers::new();
234    headers.set("authorization", &format!("Bearer {token}")).ok()?;
235    headers.set("accept", "application/vnd.github+json").ok()?;
236    // GitHub refuses API requests without one.
237    headers.set("user-agent", "lmjtfy").ok()?;
238    let mut init = worker::RequestInit::new();
239    init.with_headers(headers);
240    let request = worker::Request::new_with_init(url, &init).ok()?;
241    let mut response = worker::Fetch::Request(request).send().await.ok()?;
242    let link = response.headers().get("link").ok().flatten();
243    Some((response.status_code(), response.text().await.ok()?, link))
244}
246#[derive(Deserialize)]
247struct Listed {
248    sha: String,
249    commit: Inner,
250}
251
252#[derive(Deserialize)]
253struct Inner {
254    message: String,
255    committer: Option<Signed>,
256}
257
258#[derive(Deserialize)]
259struct Signed {
260    date: String,
261}

The commit in GitHub's one-item list, and the count from its link header.

265fn listed(body: &str, link: Option<&str>) -> Option<Commit> {
266    let mut listed: Vec<Listed> = serde_json::from_str(body).ok()?;
267    let first = (!listed.is_empty()).then(|| listed.swap_remove(0))?;
268    Some(Commit {
269        subject: first.commit.message.lines().next().unwrap_or_default().to_owned(),
270        date: first.commit.committer.map(|signed| signed.date.chars().take(10).collect()).unwrap_or_default(),
271        count: link.and_then(last_page).or(Some(1)),
272        sha: first.sha,
273    })
274}

The page number of rel="last" in a link header. With no link there is one page, which the caller counts.

278fn last_page(link: &str) -> Option<u64> {
279    let last = link.split(',').find(|part| part.contains("rel=\"last\""))?;
280    let page = last.split(['?', '&', '>']).find_map(|part| part.strip_prefix("page="))?;
281    page.parse().ok()
282}

GET /<repo>/info/refs?service=git-upload-pack. Without the service parameter this is git's old dumb protocol, which is not served.

286#[worker::send]
287pub async fn refs(State(app): State<App>, Path(segment): Path<String>, Query(asked): Query<Service>, headers: HeaderMap) -> Response<Body> {
288    let Some(repo) = Repo::named(&segment) else { return refused(StatusCode::NOT_FOUND, "No such repository.") };
289    if asked.service != UPLOAD_PACK {
290        return refused(StatusCode::FORBIDDEN, "This copy can be cloned and fetched, and nothing else.");
291    }
292    let url = format!("{}/info/refs?service={UPLOAD_PACK}", repo.upstream());
293    forward(&app, Method::GET, url, &headers, None).await
294}

POST /<repo>/git-upload-pack: the pack.

297#[worker::send]
298pub async fn upload_pack(
299    State(app): State<App>,
300    Path(segment): Path<String>,
301    axum::extract::Extension(event): axum::extract::Extension<crate::events::Event>,
302    headers: HeaderMap,
303    body: Body,
304) -> Response<Body> {
305    let Some(repo) = Repo::named(&segment) else { return refused(StatusCode::NOT_FOUND, "No such repository.") };
306    let Ok(body) = to_bytes(body, MAX_REQUEST_BYTES).await else {
307        return refused(StatusCode::PAYLOAD_TOO_LARGE, "That request is too large.");
308    };
309    let fetch = fetched(&body, headers.get(header::CONTENT_ENCODING).and_then(|value| value.to_str().ok()));
310    let url = format!("{}/{UPLOAD_PACK}", repo.upstream());
311    let response = forward(&app, Method::POST, url, &headers, Some(body.to_vec())).await;
312    let Some(fetch) = fetch.filter(|_| response.status() == StatusCode::OK) else { return response };
313    // A round of negotiation that ends a fetch is the one GitHub answers
314    // with the pack. The start of the reply is read to see, and sent on
315    // with the rest.
316    let (parts, body) = response.into_parts();
317    let mut rest = body.into_data_stream();
318    let mut start = Vec::new();
319    while start.len() < PEEK_BYTES && !packs(&start) {
320        match rest.next().await {
321            Some(Ok(chunk)) => start.extend_from_slice(&chunk),
322            _ => break,
323        }
324    }
325    if packs(&start) {
326        let which = match fetch {
327            Fetch::Clone => "clone",
328            Fetch::Pull => "pull",
329        };
330        let mut event = event.named("fetch").with(which);
331        event.status = 200.0;
332        crate::events::record(&app.env, event).await;
333        crate::archive::fetched(&app.env, repo.served(), fetch).await;
334    }
335    let first = futures_util::stream::once(async move { Ok::<_, axum::Error>(bytes::Bytes::from(start)) });
336    Response::from_parts(parts, Body::from_stream(first.chain(rest)))
337}

How much of a reply is read before giving up on finding a pack: past the acknowledgments a long negotiation can send.

341const PEEK_BYTES: usize = 64 * 1024;

Whether a reply has begun sending a pack: protocol v2's packfile section, or a v0 pack's PACK signature.

345fn packs(start: &[u8]) -> bool {
346    start.windows(9).any(|window| window == b"packfile\n") || start.windows(4).any(|window| window == b"PACK")
347}

What a fetch is, from its upload-pack request: a clone if it names no commit the client has, a pull if it does. None for anything that is not a fetch, such as protocol v2's ls-refs, which every clone and pull (and a pull with nothing new) starts with. Every round of a negotiation looks like this; the reply says which round sent the pack (packs).

354pub fn fetched(body: &[u8], encoding: Option<&str>) -> Option<Fetch> {
355    let body = match encoding {
356        Some(encoding) if encoding.eq_ignore_ascii_case("gzip") => {
357            let mut unzipped = Vec::new();
358            flate2::read::GzDecoder::new(body).take(MAX_REQUEST_BYTES as u64 * 8).read_to_end(&mut unzipped).ok()?;
359            unzipped
360        }
361        Some(_) => return None,
362        None => body.to_vec(),
363    };
364    let lines = pkt_lines(&body)?;
365    let command = lines.iter().find_map(|line| line.strip_prefix(b"command=".as_slice()));
366    if command.is_some_and(|command| command != b"fetch") {
367        return None;
368    }
369    let wants = lines.iter().any(|line| line.starts_with(b"want "));
370    let haves = lines.iter().any(|line| line.starts_with(b"have "));
371    match (wants, haves) {
372        (true, false) => Some(Fetch::Clone),
373        (true, true) => Some(Fetch::Pull),
374        (false, _) => None,
375    }
376}

Git's pkt-lines: four hex digits of length, then the payload, without its newline. Flush, delimiter and response-end packets (0000, 0001, 0002) carry nothing. None if the body is not pkt-lines.

381fn pkt_lines(body: &[u8]) -> Option<Vec<&[u8]>> {
382    let mut lines = Vec::new();
383    let mut at = 0;
384    while at < body.len() {
385        let length = usize::from_str_radix(std::str::from_utf8(body.get(at..at + 4)?).ok()?, 16).ok()?;
386        if length < 4 {
387            at += 4;
388            continue;
389        }
390        let line = body.get(at + 4..at + length)?;
391        lines.push(line.strip_suffix(b"\n").unwrap_or(line));
392        at += length;
393    }
394    Some(lines)
395}
397async fn forward(app: &App, method: Method, url: String, headers: &HeaderMap, body: Option<Vec<u8>>) -> Response<Body> {
398    let Ok(token) = app.env.secret(TOKEN).map(|secret| secret.to_string()) else {
399        return refused(StatusCode::SERVICE_UNAVAILABLE, "Cloning is not set up here yet.");
400    };
401    let mut request = http::Request::builder().method(method).uri(url);
402    for (name, value) in forwarded(headers) {
403        request = request.header(name, value);
404    }
405    request = request.header(header::AUTHORIZATION, authorization(&token));
406    let request = match request.body(Body::from(body.unwrap_or_default())) {
407        Ok(request) => request,
408        Err(_) => return refused(StatusCode::BAD_REQUEST, "That request could not be read."),
409    };
410    let sent = match worker::Request::try_from(request) {
411        Ok(request) => worker::Fetch::Request(request).send().await,
412        Err(error) => Err(error),
413    };
414    match sent {
415        // Streamed back as GitHub sends it: a pack can be megabytes.
416        Ok(response) => response.into(),
417        Err(_) => refused(StatusCode::BAD_GATEWAY, "GitHub did not answer."),
418    }
419}

The client's headers that are passed on. Its own authorization, if it sent one, is not: the token is the only credential GitHub sees.

423fn forwarded(headers: &HeaderMap) -> impl Iterator<Item = (&'static str, &header::HeaderValue)> {
424    FORWARDED.into_iter().filter_map(|name| headers.get(name).map(|value| (name, value)))
425}

GitHub takes a token over git's HTTP as the password of Basic auth.

428fn authorization(token: &str) -> String {
429    format!("Basic {}", STANDARD.encode(format!("x-access-token:{token}")))
430}
432pub fn refused(status: StatusCode, why: &str) -> Response<Body> {
433    Response::builder()
434        .status(status)
435        .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
436        .body(Body::from(format!("{why}\n")))
437        .expect("a static response")
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn only_the_listed_repositories_are_served() {
446        assert_eq!(Repo::named("lmjtfy.git"), Some(Repo::Lmjtfy));
447        assert_eq!(Repo::named("jevcrates.git"), Some(Repo::Jevcrates));
448        assert_eq!(Repo::named("postjevsql.git"), Some(Repo::Postjevsql));
449        assert_eq!(Repo::named("jevhooks.git"), Some(Repo::Jevhooks));
450        assert_eq!(Repo::named("jevsnes.git"), Some(Repo::Jevsnes));
451        assert_eq!(Repo::named("lmjtfy"), None);
452        assert_eq!(Repo::named("nixos-config.git"), None);
453        assert_eq!(Repo::named("..%2Fnixos-config.git"), None);
454    }
455
456    #[test]
457    fn the_submodule_resolves_to_a_served_repository() {
458        // `.gitmodules` names jevcrates relative to lmjtfy, so the path it
459        // resolves to here must be one that is served.
460        let gitmodules = include_str!("../../../.gitmodules");
461        assert!(gitmodules.contains("url = ../jevcrates.git"), "{gitmodules}");
462        assert_eq!(Repo::named("jevcrates.git"), Some(Repo::Jevcrates));
463        for (mount, repo) in Repo::Lmjtfy.submodules() {
464            assert!(gitmodules.contains(&format!("path = {mount}")), "{mount}");
465            assert!(gitmodules.contains(&format!("url = ../{}", repo.served())), "{mount}");
466        }
467    }
468
469    #[test]
470    fn a_repository_with_submodules_is_cloned_with_them() {
471        assert_eq!(Repo::Lmjtfy.command("https://x"), "git clone --recurse-submodules https://x/lmjtfy.git");
472        assert_eq!(Repo::Jevcrates.command("https://x"), "git clone https://x/jevcrates.git");
473        // Every submodule is a served repository, so a clone from here can
474        // fetch it from here.
475        for repo in Repo::ALL {
476            for (_, inner) in repo.submodules() {
477                assert!(Repo::ALL.contains(inner), "{repo:?}");
478            }
479        }
480    }
481
482    #[test]
483    fn the_clients_own_credentials_are_not_forwarded() {
484        let mut headers = HeaderMap::new();
485        headers.insert("authorization", "Basic c29tZW9uZQ==".parse().unwrap());
486        headers.insert("cookie", "a=b".parse().unwrap());
487        headers.insert("git-protocol", "version=2".parse().unwrap());
488        headers.insert("user-agent", "git/2.51.0".parse().unwrap());
489        let names: Vec<_> = forwarded(&headers).map(|(name, _)| name).collect();
490        assert_eq!(names, ["git-protocol", "user-agent"]);
491    }
492
493    #[test]
494    fn the_token_is_the_basic_password() {
495        let decoded = STANDARD.decode(authorization("t0k").trim_start_matches("Basic ")).unwrap();
496        assert_eq!(decoded, b"x-access-token:t0k");
497    }
498
499    #[test]
500    fn the_latest_commit_is_read_from_githubs_list() {
501        let body = r#"[{"sha":"4d93353abc","commit":{"message":"clone: the token\n\nmore","committer":{"date":"2026-10-02T23:59:31Z"}}}]"#;
502        let link = r#"<https://api.github.com/repositories/1/commits?sha=main&per_page=1&page=2>; rel="next", <https://api.github.com/repositories/1/commits?sha=main&per_page=1&page=140>; rel="last""#;
503        let commit = listed(body, Some(link)).unwrap();
504        assert_eq!(commit.short(), "4d93353");
505        assert_eq!(commit.subject, "clone: the token");
506        assert_eq!(commit.date, "2026-10-02");
507        assert_eq!(commit.count, Some(140));
508        assert_eq!(listed(body, None).unwrap().count, Some(1));
509        assert_eq!(listed("[]", None), None);
510    }
511
512    fn pkt(lines: &[&str]) -> Vec<u8> {
513        let mut out = Vec::new();
514        for line in lines {
515            match *line {
516                "0000" | "0001" => out.extend_from_slice(line.as_bytes()),
517                line => out.extend_from_slice(format!("{:04x}{line}\n", line.len() + 5).as_bytes()),
518            }
519        }
520        out
521    }
522
523    #[test]
524    fn a_clone_and_a_pull_are_told_apart() {
525        let want = "want 4d93353131e2d6b5a1b1a3a3c5f2f6a7b8c9d0e1";
526        let have = "have 886ece71e2d6b5a1b1a3a3c5f2f6a7b8c9d0e1f2";
527        // Protocol v2, as git 2.x sends it.
528        let clone = pkt(&["command=fetch", "agent=git/2.51.0", "0001", "thin-pack", want, "done", "0000"]);
529        assert_eq!(fetched(&clone, None), Some(Fetch::Clone));
530        let pull = pkt(&["command=fetch", "0001", want, have, "done", "0000"]);
531        assert_eq!(fetched(&pull, None), Some(Fetch::Pull));
532        // Without `done`: the server may still send the pack, and the reply
533        // says whether it did.
534        let round = pkt(&["command=fetch", "0001", want, have, "0000"]);
535        assert_eq!(fetched(&round, None), Some(Fetch::Pull));
536        let refs = pkt(&["command=ls-refs", "0001", "peel", "symrefs", "0000"]);
537        assert_eq!(fetched(&refs, None), None);
538        // Protocol v0, with capabilities on the first want.
539        let old = pkt(&[&format!("{want} multi_ack side-band-64k"), "0000", "done"]);
540        assert_eq!(fetched(&old, None), Some(Fetch::Clone));
541        assert_eq!(fetched(b"not pkt lines", None), None);
542    }
543
544    #[test]
545    fn only_a_reply_that_sends_the_pack_counts() {
546        assert!(packs(&pkt(&["acknowledgments", "ACK 886ece7", "ready", "0001", "packfile", "0000"])));
547        assert!(packs(b"0008NAK\n0031\x01PACK\x00\x00\x00\x02"));
548        assert!(!packs(&pkt(&["acknowledgments", "NAK", "0000"])));
549        assert!(!packs(&pkt(&["886ece7 HEAD symref-target:refs/heads/main", "0000"])));
550    }
551
552    #[test]
553    fn a_gzipped_request_is_read() {
554        use std::io::Write;
555        let pull = pkt(&["command=fetch", "0001", "want a", "have b", "done", "0000"]);
556        let mut gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
557        gzip.write_all(&pull).unwrap();
558        assert_eq!(fetched(&gzip.finish().unwrap(), Some("gzip")), Some(Fetch::Pull));
559        assert_eq!(fetched(&pull, Some("br")), None);
560    }
561
562    #[test]
563    fn the_command_clones_the_submodule_too() {
564        assert_eq!(command("https://lmjtfy.fun"), "git clone --recurse-submodules https://lmjtfy.fun/lmjtfy.git");
565    }
566}