From 1cfd2fa1ae6415ebed8d740ef9cfeeeafa1d6c7b Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Fri, 25 Sep 2026 02:00:12 +0200 Subject: [PATCH 1/2] Read a symlinked destination's ignores where it physically is Every other reading of the destination follows the link into the repository its target is in, while check-ignore asked the repository the link sits in, so that one's ignores could plant a repository in the other's tracked tree. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014jwcxdPLFmuWiwMg7PKWsH --- CHANGELOG.md | 2 +- initializers/js/README.md | 2 +- initializers/js/lib/git.mjs | 19 ++++++++++++------- initializers/js/test/git.test.mjs | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a58dea2..b34911f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- **`create-method-app` at a path another repository ignores**: a destination inside another repository's work tree that the repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, now gets a repository of its own on `main` with the pristine commit, and the `git:` line names the repository that ignores it. The initializer used to report such a project as new files in the enclosing repository and make no repository, which left it under no version control at all; a project made that way earlier can be given one with `git init` and a first commit. A directory the repository does not ignore still gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, and the `git:` line now says the project is under no version control instead of calling it new files of that repository. +- **`create-method-app` at a path another repository ignores**: a destination inside another repository's work tree that the repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, now gets a repository of its own on `main` with the pristine commit, and the `git:` line names the repository that ignores it. The initializer used to report such a project as new files in the enclosing repository and make no repository, which left it under no version control at all; a project made that way earlier can be given one with `git init` and a first commit. A directory the repository does not ignore still gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, and the `git:` line now says the project is under no version control instead of calling it new files of that repository. A destination that is a symlink is judged by the repository its target is in, not the one the link sits in. ## [v0.5.3] - 2026-09-24 diff --git a/initializers/js/README.md b/initializers/js/README.md index a76aaaf..5fb485c 100644 --- a/initializers/js/README.md +++ b/initializers/js/README.md @@ -50,7 +50,7 @@ The initializer reads git before it writes anything: | Inside another repository's work tree | No repository and no commit: the project is new files in that repository. `--no-create` lets you commit the template there first | | Inside a checkout of `pipelex-method-apps` or of a starter | Refused, `--no-git` included whenever git is on the PATH | -A path the enclosing repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, is one it does not version, so a project there gets a repository of its own rather than no version control at all. Every source git reads counts: the enclosing repository's `.gitignore` files, its `.git/info/exclude`, and your `core.excludesFile`. What counts is the directory itself, which a missing destination is made for a moment to let git read: a directory the enclosing repository does not ignore gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, since a repository there would still show in the enclosing one's `git status`, and the `git:` line then says the project is under no version control. +A path the enclosing repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, is one it does not version, so a project there gets a repository of its own rather than no version control at all. Every source git reads counts: the enclosing repository's `.gitignore` files, its `.git/info/exclude`, and your `core.excludesFile`. What counts is the directory itself, which a missing destination is made for a moment to let git read: a directory the enclosing repository does not ignore gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, since a repository there would still show in the enclosing one's `git status`, and the `git:` line then says the project is under no version control. A destination that is a symlink is read where it points: the repository whose work tree holds its target is the one whose ignores count. A commit needs a git identity, and a missing one is refused before anything is written. When git shows none outside a repository, the initializer asks again inside a throwaway repository at the destination, which it removes before going on, so an identity given only by an `includeIf "gitdir:…"` section is found. At a path another repository ignores, it always asks inside that throwaway repository, since an identity set in the enclosing repository's own configuration does not reach the new one. diff --git a/initializers/js/lib/git.mjs b/initializers/js/lib/git.mjs index f015b37..10335b6 100644 --- a/initializers/js/lib/git.mjs +++ b/initializers/js/lib/git.mjs @@ -116,16 +116,21 @@ export function readGit({ dest, from, destExists, destHasGit, env }) { * directory is named from its parent, with no slash, since asked from inside * it as `./` git tests it with one. The name starts with `./` because git * reads a leading `:` as pathspec magic, and `--literal-pathspecs` is refused - * by `check-ignore`. Any answer but a plain yes, a directory that cannot be - * made included, reads as not ignored, since mistaking a tracked destination - * for an ignored one would plant a repository in the user's tracked tree. A - * directory that holds a tracked path reads as not ignored whatever the - * patterns say, but a destination the preflight accepts holds none. + * by `check-ignore`. The directory is named where it physically is: a + * destination that is a symlink sits in one repository's work tree and points + * into another's, the one every other reading of the destination sees, and + * that one is the repository whose ignores count. Any answer but a plain yes, a + * directory that cannot be made or resolved included, reads as not ignored, + * since mistaking a tracked destination for an ignored one would plant a + * repository in the user's tracked tree. A directory that holds a tracked path + * reads as not ignored whatever the patterns say, whether or not its files are + * still on disk. */ function ignores({ dest, from, env }) { return withDirectories({ dest, from }, false, () => { - const name = `./${path.basename(dest)}`; - return git(["check-ignore", "-q", "--", name], { cwd: path.dirname(dest), env }).status === 0; + const real = fs.realpathSync(dest); + const name = `./${path.basename(real)}`; + return git(["check-ignore", "-q", "--", name], { cwd: path.dirname(real), env }).status === 0; }); } diff --git a/initializers/js/test/git.test.mjs b/initializers/js/test/git.test.mjs index 38b10fd..dc990ec 100644 --- a/initializers/js/test/git.test.mjs +++ b/initializers/js/test/git.test.mjs @@ -104,6 +104,23 @@ describe("readGit inside another repository's work tree", () => { }); } + it("reads a symlinked destination by the ignores of the repository it points into", () => { + const { root, repo, env } = enclosing(["link"]); + const theirs = path.join(root, "other"); + fs.mkdirSync(theirs); + git(theirs, ["init", "-q", "-b", "main"], env); + fs.writeFileSync(path.join(theirs, "README.md"), "other\n"); + git(theirs, ["add", "-A"], env); + git(theirs, ["commit", "-q", "-m", "Other first commit"], env); + const target = path.join(theirs, "apps", "new"); + fs.mkdirSync(target, { recursive: true }); + const link = path.join(repo, "link"); + fs.symlinkSync(target, link); + assert.deepEqual(readExisting(link, env), { kind: "inside", toplevel: theirs }); + fs.writeFileSync(path.join(theirs, ".gitignore"), "apps/new/\n"); + assert.deepEqual(readExisting(link, env), { kind: "ignored", toplevel: theirs }); + }); + it("reads a destination under a tracked path as inside", () => { const { repo, env } = enclosing(["*.log"]); assert.deepEqual(readMissing(path.join(repo, "src", "my-app"), env), { From 6a4ceb41d3d347416a5a8d795019272cc20d70f6 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Fri, 25 Sep 2026 02:05:01 +0200 Subject: [PATCH 2/2] Commit the symlink test's second repository with the setup identity Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014jwcxdPLFmuWiwMg7PKWsH --- initializers/js/test/git.test.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/initializers/js/test/git.test.mjs b/initializers/js/test/git.test.mjs index dc990ec..b795e5f 100644 --- a/initializers/js/test/git.test.mjs +++ b/initializers/js/test/git.test.mjs @@ -106,12 +106,13 @@ describe("readGit inside another repository's work tree", () => { it("reads a symlinked destination by the ignores of the repository it points into", () => { const { root, repo, env } = enclosing(["link"]); + const setup = setupEnv(root); const theirs = path.join(root, "other"); fs.mkdirSync(theirs); - git(theirs, ["init", "-q", "-b", "main"], env); + git(theirs, ["init", "-q", "-b", "main"], setup); fs.writeFileSync(path.join(theirs, "README.md"), "other\n"); - git(theirs, ["add", "-A"], env); - git(theirs, ["commit", "-q", "-m", "Other first commit"], env); + git(theirs, ["add", "-A"], setup); + git(theirs, ["commit", "-q", "-m", "Other first commit"], setup); const target = path.join(theirs, "apps", "new"); fs.mkdirSync(target, { recursive: true }); const link = path.join(repo, "link");