From 679a06ce25f8bd723d98a2d7165e424df0fbd687 Mon Sep 17 00:00:00 2001 From: QuangNguyen2609 Date: Thu, 15 Oct 2026 12:37:18 +1030 Subject: [PATCH] fix(git): upgrade just-git to 1.10.0 --- .changeset/just-git-110.md | 5 + package.json | 2 +- pnpm-lock.yaml | 10 +- src/api/commands/git-command.ts | 7 +- .../unit/git-command.regressions.test.ts | 105 ++++++++++++++++++ 5 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 .changeset/just-git-110.md create mode 100644 src/api/tests/unit/git-command.regressions.test.ts diff --git a/.changeset/just-git-110.md b/.changeset/just-git-110.md new file mode 100644 index 0000000..52c0f49 --- /dev/null +++ b/.changeset/just-git-110.md @@ -0,0 +1,5 @@ +--- +"sql-fs-api": patch +--- + +Upgrade just-git to 1.10.0 for mode-only status, diff, checkout, and merge fixes, ls-tree support, and summary output. diff --git a/package.json b/package.json index bb71a00..ad9303d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "ioredis": "^5.4.0", "jose": "^6.2.12", "just-bash": "^3.6.0", - "just-git": "^1.9.1", + "just-git": "^1.10.0", "lru-cache": "^11.0.0", "postgres": "^3.4.0", "zod": "^3.24.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab4a722..9263ea6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^3.6.0 version: 3.6.0(patch_hash=7767ed5b1605abaf5ae15082347fa925b0dfecc7fe870fbcf2c30726c8cbb6b5) just-git: - specifier: ^1.9.1 - version: 1.9.1 + specifier: ^1.10.0 + version: 1.10.0 lru-cache: specifier: ^11.0.0 version: 11.3.5 @@ -1765,8 +1765,8 @@ packages: engines: {node: '>=20.19'} hasBin: true - just-git@1.9.1: - resolution: {integrity: sha512-UuyDJT4bbPTE5QfbA+QjQ/knKfvk0BSuIzRRAbE5DweBCEDd/NnIv+iNMILqyC0fmGj1abDoDO6Ngxqyb1vXZw==} + just-git@1.10.0: + resolution: {integrity: sha512-YsQAKD/4DRMI1a4y8q2Q0yYbO+PdgQDgRDexdAysMkQrZm0SEMB8/fEK2CsJn3IA4VK5TqGxPmKc3xZOncBvrg==} knip@6.4.1: resolution: {integrity: sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw==} @@ -3780,7 +3780,7 @@ snapshots: transitivePeerDependencies: - supports-color - just-git@1.9.1: {} + just-git@1.10.0: {} knip@6.4.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: diff --git a/src/api/commands/git-command.ts b/src/api/commands/git-command.ts index 48ddf91..546f8c7 100644 --- a/src/api/commands/git-command.ts +++ b/src/api/commands/git-command.ts @@ -13,8 +13,8 @@ * is the path just-git actually resolved. A script may background several clones, so each * invocation gets its own scope via AsyncLocalStorage. * - * just-git's checkout also never sets the executable bit (blindmansion/just-git#10), so every - * file git writes is given the mode its index entry records until upstream fixes that. + * just-git 1.10 handles mode-only changes. The wrapper also reapplies index modes for files + * written by checkout and merge, including explicitly selected unmerged stages. */ import { AsyncLocalStorage } from "node:async_hooks"; @@ -217,8 +217,7 @@ async function indexModes( /** * Give each regular file git wrote its index mode, merging unmerged stages as Git does. * Only those files change, as with real git, so a caller's own uncommitted chmod on any other file - * stays. A branch switch where a file differs only in mode is not covered: just-git neither - * rewrites that file nor updates its index entry, so there is no correct mode to read. + * stays. just-git handles branch switches where a file differs only in mode. */ async function applyIndexModes( fs: IFileSystem, diff --git a/src/api/tests/unit/git-command.regressions.test.ts b/src/api/tests/unit/git-command.regressions.test.ts new file mode 100644 index 0000000..3c0adbe --- /dev/null +++ b/src/api/tests/unit/git-command.regressions.test.ts @@ -0,0 +1,105 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { type BashOverSqlFs, bashOverSqlFs } from "../../../sql-fs/tests/fixtures/bash-over-sqlfs.js"; +import { createGitCommand } from "../../commands/git-command.js"; + +const IDENTITY = { + GIT_AUTHOR_NAME: "a", + GIT_AUTHOR_EMAIL: "a@b.c", + GIT_COMMITTER_NAME: "a", + GIT_COMMITTER_EMAIL: "a@b.c", +}; + +const SEED = [ + "mkdir repo && cd repo && git init -q", + "printf 'echo ran\\n' > run.sh && chmod 755 run.sh", + "git add -A && git commit -q -m init", + "git checkout -q -b plain && chmod 644 run.sh && git commit -q -am plain && git checkout -q main", +].join(" && "); + +describe("git wrapper with just-git 1.10 regressions", () => { + let env: BashOverSqlFs; + + async function sh(script: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { + const r = await env.bash.exec(script); + return { stdout: r.stdout, stderr: r.stderr, exitCode: r.exitCode }; + } + + beforeEach(async () => { + env = await bashOverSqlFs({ env: IDENTITY, customCommands: [createGitCommand({ network: false })] }); + expect((await sh(SEED)).exitCode).toBe(0); + }); + + it("reports a chmod-only change in status and diff", async () => { + const r = await sh("cd repo && chmod 644 run.sh && git status --short && git diff"); + expect(r).toEqual({ + stdout: " M run.sh\ndiff --git a/run.sh b/run.sh\nold mode 100755\nnew mode 100644\n", + stderr: "", + exitCode: 0, + }); + }); + + it("applies the mode when switching to a branch where only the mode differs", async () => { + expect((await sh("cd repo && stat -c %a run.sh")).stdout).toBe("755\n"); + await sh("cd repo && git checkout -q plain"); + expect((await sh("cd repo && stat -c %a run.sh && git status --short")).stdout).toBe("644\n"); + await sh("cd repo && git checkout -q main"); + expect((await sh("cd repo && stat -c %a run.sh && git status --short")).stdout).toBe("755\n"); + }); + + it("refuses to switch over a local chmod-only change", async () => { + const r = await sh("cd repo && chmod 644 run.sh && git checkout plain"); + expect(r.exitCode).toBe(1); + expect(r.stderr).toContain("Your local changes to the following files would be overwritten by checkout:"); + expect((await sh("cd repo && git branch --show-current")).stdout).toBe("main\n"); + }); + + it("lists a tree with git ls-tree", async () => { + expect(await sh("cd repo && git ls-tree --name-only plain && git ls-tree HEAD | cut -d' ' -f1-2")).toEqual({ + stdout: "run.sh\n100755 blob\n", + stderr: "", + exitCode: 0, + }); + }); + + it("recurses, filters by path and resolves : in git ls-tree", async () => { + await sh( + "cd repo && mkdir -p src/lib && echo a > src/a && echo b > src/lib/b && git add src && git commit -q -m src", + ); + const r = await sh( + "cd repo && git ls-tree --name-only HEAD && git ls-tree -r --name-only HEAD && " + + "git ls-tree --name-only HEAD src/lib && git ls-tree -r --name-only HEAD:src", + ); + expect(r).toEqual({ + stdout: "run.sh\nsrc\n" + "run.sh\nsrc/a\nsrc/lib/b\n" + "src/lib\n" + "a\nlib/b\n", + stderr: "", + exitCode: 0, + }); + }); + + it("keeps a mode-only change from the merged branch", async () => { + await sh("cd repo && git checkout -q -b side plain && chmod 755 run.sh && git commit -q -am exec"); + await sh("cd repo && git checkout -q plain && echo n > n && git add n && git commit -q -m n"); + const r = await sh("cd repo && git merge -q -m mg side && git ls-files -s run.sh | cut -c1-6 && stat -c %a run.sh"); + expect(r).toEqual({ stdout: "100755\n755\n", stderr: "", exitCode: 0 }); + }); + + it("gives a conflicted file the merged mode", async () => { + await sh( + "cd repo && git checkout -q -b side plain && echo theirs > run.sh && chmod 755 run.sh && git commit -q -am t", + ); + await sh("cd repo && git checkout -q plain && echo ours > run.sh && git commit -q -am o"); + const r = await sh( + "cd repo && git merge -q side; echo rc=$?; git ls-files -s run.sh | cut -c1-6 && stat -c %a run.sh", + ); + expect(r.stdout.split("\n").slice(-6)).toEqual(["rc=1", "100644", "100644", "100755", "755", ""]); + }); + + it("prints git show --summary", async () => { + await sh("cd repo && git mv run.sh go.sh && echo n > n && git add n && git commit -q -m sum"); + expect(await sh("cd repo && git show --summary --format=%s HEAD")).toEqual({ + stdout: "sum\n\n rename run.sh => go.sh (100%)\n create mode 100644 n\n", + stderr: "", + exitCode: 0, + }); + }); +});