Skip to content

Commit 4215785

Browse files
committed
computer/git: clone full history, and fill three CLI gaps
clone defaulted to depth 1. That made it fast, but it quietly cost the caller their history: pushing such a clone somewhere else sent only the single commit it had fetched, reported success, and left a remote whose tip hash matched while every earlier commit was missing. A content diff against that remote passes, so nothing surfaces the loss until someone looks for a parent that is not there. Default to full history and leave the shallow case to an explicit --depth, where the caller is choosing speed knowingly. Three smaller gaps go with it. cat-file grew -t and -s, which readObject already had the type and size for. log grew --format and its --pretty alias over the commonly scripted placeholders, leaving an unrecognized one as written so it shows up in the output rather than vanishing. help grew a per-command form; it previously ignored its argument and reprinted the list, which left no way to discover a command's flags except guessing and reading the exit code.
1 parent 25c603a commit 4215785

7 files changed

Lines changed: 351 additions & 28 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@cloudflare/computer": minor
3+
---
4+
5+
`git clone` now fetches the full history by default instead of a single commit. The shallow default was faster, but a caller who cloned a repository and then pushed it somewhere else sent only the one commit it had fetched: the push reported success and the remote's tip matched, while every earlier commit was missing. Pass `--depth` to ask for a shallow clone when the history genuinely is not needed.
6+
7+
`git cat-file` gained `-t` and `-s` to report an object's type and size, alongside the existing `-p`. Exactly one of the three is required, as in real git.
8+
9+
`git log` gained `--format` and its alias `--pretty`, expanding the placeholders `%H`, `%h`, `%s`, `%b`, `%an`, `%ae`, `%ad`, `%cn`, `%ce`, `%cd`, and `%%`, plus the named format `oneline`. A placeholder outside that set is left as written so it is visible in the output rather than silently dropped.
10+
11+
`git help <command>` now prints the usage line for one command instead of ignoring its argument and reprinting the full list. Only the flags this wrapper accepts are listed, so the output says what works here rather than what real git would take.

docs/13_git_interface.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ clean fetch reset rev-parse switch
4444
update-ref
4545
```
4646

47+
`git help` lists these, and `git help <command>` prints the usage line for
48+
one of them, covering only the flags this wrapper accepts. Asking for a
49+
command that is not supported reports that rather than reprinting the list.
50+
4751
Global options accepted before the subcommand:
4852

4953
- **`-C <path>`** — run the subcommand as though invoked from
@@ -412,6 +416,7 @@ ws.git.log({
412416
| `-n <N>` | `depth` |
413417
| `-<N>` (e.g. `-1`, `-5`) | `depth` |
414418
| `--oneline` | (CLI formatter) |
419+
| `--format=<spec>` / `--pretty=<spec>` | (CLI formatter) |
415420
| `<ref>` (positional) | `ref` |
416421

417422
The positional `<ref>` accepts revision suffixes (`HEAD~2`,
@@ -423,6 +428,12 @@ blocks; `--oneline` collapses each entry to `<short-oid>
423428
message, tree, parent, author, committer) so callers can
424429
format their own way.
425430

431+
`--format=<spec>` (and its alias `--pretty=<spec>`) expands the
432+
placeholders `%H`, `%h`, `%s`, `%b`, `%an`, `%ae`, `%ad`, `%cn`, `%ce`,
433+
`%cd`, and `%%`, plus the named format `oneline`. A placeholder outside
434+
that set is left as written, so an unsupported one is visible in the
435+
output rather than silently dropped.
436+
426437
*Not mapped:* `--graph`, `--all`, `--since`, `--until`,
427438
`-p`, `--stat`, `--follow`, `--reverse`.
428439

@@ -545,7 +556,7 @@ ws.git.clone({
545556
dir?: string,
546557
ref?: string,
547558
paths?: string[], // partial checkout only
548-
depth?: number, // default 1
559+
depth?: number, // default: full history
549560
singleBranch?: boolean,
550561
noTags?: boolean,
551562
headers?: Record<string, string>,
@@ -564,6 +575,11 @@ ws.git.clone({
564575
| `<url>` (positional) | `url` |
565576
| `<dir>` (positional) | `dir` |
566577

578+
Clone fetches the full history unless `--depth` asks otherwise. A shallow
579+
clone is faster, but it can only push the commits it actually fetched: the
580+
push succeeds and the remote's tip matches, while every earlier commit is
581+
missing. Ask for a shallow clone when the history genuinely is not needed.
582+
567583
When `<dir>` is omitted, the CLI derives it from the last path
568584
segment of the URL, stripping a trailing `.git` — `git clone
569585
https://github.com/owner/repo.git` lands in `./repo`, matching
@@ -869,19 +885,21 @@ readback that the typed API serves better directly.
869885
### `cat-file`
870886

871887
```
872-
git cat-file -p <oid>[:<path>]
888+
git cat-file (-p|-t|-s) <oid>[:<path>]
873889
```
874890

875891
```ts
876892
ws.git.catFile({
877893
dir?: string,
878894
oid: string,
879895
filepath?: string,
880-
}): Promise<{ oid: string; bytes: Uint8Array }>
896+
}): Promise<{ oid: string; bytes: Uint8Array; type?: string }>
881897
```
882898

883-
Supports the `<oid>:<path>` shorthand for tree subreads.
884-
*Not mapped:* `-t` type, `-s` size, `--batch`.
899+
Supports the `<oid>:<path>` shorthand for tree subreads. `-p` prints the
900+
object's bytes, `-t` its type, and `-s` its size in bytes; exactly one of
901+
the three is required, as in real git.
902+
*Not mapped:* `--batch`.
885903

886904
### `update-ref`
887905

packages/computer/src/git/cli.test.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1754,11 +1754,11 @@ describe("runGitCli — cat-file", () => {
17541754
});
17551755
});
17561756

1757-
it("without -p is an error", async () => {
1757+
it("without a mode flag is an error", async () => {
17581758
const { client } = fakeClient();
17591759
const res = await runGitCli(client, { argv: ["cat-file", "a".repeat(40)] });
17601760
expect(res.exitCode).toBe(129);
1761-
expect(res.stderr).toContain("only -p is supported");
1761+
expect(res.stderr).toContain("one of -p, -t, or -s is required");
17621762
});
17631763
});
17641764

@@ -2340,3 +2340,110 @@ describe("runGitCli — end-to-end against an in-process Workspace", () => {
23402340
expect(res.stdout).toBe("");
23412341
});
23422342
});
2343+
2344+
describe("runGitCli — cat-file type and size", () => {
2345+
it("-t reports the object type", async () => {
2346+
const { client } = fakeClient(
2347+
{},
2348+
{
2349+
catFile: () => ({
2350+
oid: "a".repeat(40),
2351+
bytes: new TextEncoder().encode("hello\n"),
2352+
type: "commit" as const,
2353+
}),
2354+
},
2355+
);
2356+
const res = await runGitCli(client, { argv: ["cat-file", "-t", "a".repeat(40)] });
2357+
expect(res.exitCode).toBe(0);
2358+
expect(res.stdout).toBe("commit\n");
2359+
});
2360+
2361+
it("-s reports the object size in bytes", async () => {
2362+
const { client } = fakeClient(
2363+
{},
2364+
{
2365+
catFile: () => ({
2366+
oid: "a".repeat(40),
2367+
bytes: new TextEncoder().encode("hello\n"),
2368+
}),
2369+
},
2370+
);
2371+
const res = await runGitCli(client, { argv: ["cat-file", "-s", "a".repeat(40)] });
2372+
expect(res.exitCode).toBe(0);
2373+
expect(res.stdout).toBe("6\n");
2374+
});
2375+
2376+
it("rejects combining -p and -t", async () => {
2377+
const { client } = fakeClient();
2378+
const res = await runGitCli(client, { argv: ["cat-file", "-p", "-t", "a".repeat(40)] });
2379+
expect(res.exitCode).toBe(129);
2380+
expect(res.stderr).toMatch(/mutually exclusive/);
2381+
});
2382+
});
2383+
2384+
describe("runGitCli — help for one command", () => {
2385+
it("prints usage for a named command", async () => {
2386+
const { client } = fakeClient();
2387+
const res = await runGitCli(client, { argv: ["help", "log"] });
2388+
expect(res.exitCode).toBe(0);
2389+
expect(res.stdout).toMatch(/^usage: git log /);
2390+
expect(res.stdout).toMatch(/--oneline/);
2391+
});
2392+
2393+
it("still prints the command list when given no argument", async () => {
2394+
const { client } = fakeClient();
2395+
const res = await runGitCli(client, { argv: ["help"] });
2396+
expect(res.exitCode).toBe(0);
2397+
expect(res.stdout).toMatch(/Supported workspace git commands/);
2398+
});
2399+
2400+
it("reports an unknown command rather than reprinting the list", async () => {
2401+
const { client } = fakeClient();
2402+
const res = await runGitCli(client, { argv: ["help", "rebase"] });
2403+
expect(res.exitCode).toBe(1);
2404+
expect(res.stderr).toMatch(/no help available for 'rebase'/);
2405+
});
2406+
});
2407+
2408+
describe("runGitCli — log --format", () => {
2409+
const sample = (oid: string, msg: string): CommitView => ({
2410+
oid,
2411+
message: msg,
2412+
tree: "",
2413+
parent: [],
2414+
author: { name: "A", email: "a@x", timestamp: 1_700_000_000, timezoneOffset: 0 },
2415+
committer: { name: "C", email: "c@x", timestamp: 1_700_000_000, timezoneOffset: 0 },
2416+
});
2417+
2418+
it("expands the common placeholders", async () => {
2419+
const { client } = fakeClient({}, { log: () => [sample("a".repeat(40), "second")] });
2420+
const res = await runGitCli(client, { argv: ["log", "--format=%h %s (%an)"] });
2421+
expect(res.exitCode).toBe(0);
2422+
expect(res.stdout).toBe("aaaaaaa second (A)\n");
2423+
});
2424+
2425+
it("expands the full hash and the committer separately from the author", async () => {
2426+
const { client } = fakeClient({}, { log: () => [sample("a".repeat(40), "x")] });
2427+
const res = await runGitCli(client, { argv: ["log", "--format=%H|%an|%cn"] });
2428+
expect(res.stdout).toBe(`${"a".repeat(40)}|A|C\n`);
2429+
});
2430+
2431+
it("treats --pretty as an alias and understands the oneline format", async () => {
2432+
const { client } = fakeClient({}, { log: () => [sample("a".repeat(40), "second")] });
2433+
const res = await runGitCli(client, { argv: ["log", "--pretty=oneline"] });
2434+
expect(res.exitCode).toBe(0);
2435+
expect(res.stdout).toBe("aaaaaaa second\n");
2436+
});
2437+
2438+
it("leaves an unknown placeholder as written", async () => {
2439+
const { client } = fakeClient({}, { log: () => [sample("a".repeat(40), "x")] });
2440+
const res = await runGitCli(client, { argv: ["log", "--format=%h %zz"] });
2441+
expect(res.stdout).toBe("aaaaaaa %zz\n");
2442+
});
2443+
2444+
it("rejects --format combined with --oneline", async () => {
2445+
const { client } = fakeClient();
2446+
const res = await runGitCli(client, { argv: ["log", "--oneline", "--format=%h"] });
2447+
expect(res.exitCode).toBe(129);
2448+
});
2449+
});

0 commit comments

Comments
 (0)