diff --git a/packages/validation/src/workspace.ts b/packages/validation/src/workspace.ts index d5cfb9c..48310b1 100644 --- a/packages/validation/src/workspace.ts +++ b/packages/validation/src/workspace.ts @@ -21,14 +21,18 @@ export interface CreateNodeValidationWorkspaceOptions { export function createNodeValidationWorkspace(options: CreateNodeValidationWorkspaceOptions): ValidationWorkspace { const repoRoot = resolve(options.repoRoot); + const gitPrefix = resolveGitPrefix(repoRoot); const skippedPathSegments = new Set(options.skippedPathSegments ?? []); return { - readFile: (path, context) => readWorkspaceFile(repoRoot, path, context), + readFile: (path, context) => readWorkspaceFile(repoRoot, gitPrefix, path, context), listFiles: (context) => filterFileSet(listWorkspaceFiles(repoRoot, context, skippedPathSegments), skippedPathSegments), listChangedFiles: (baseRef) => filterFileSet(listChangedFiles(repoRoot, baseRef), skippedPathSegments), listTreeFiles: (treeRef, changedFrom) => filterFileSet(listTreeFiles(repoRoot, treeRef, changedFrom), skippedPathSegments), listStagedFiles: () => - filterFileSet(listDiffFiles(repoRoot, ["diff", "--cached", "--name-status", "-z", "--find-renames", "--"]), skippedPathSegments), + filterFileSet( + listDiffFiles(repoRoot, ["diff", "--relative", "--cached", "--name-status", "-z", "--find-renames", "--", "."]), + skippedPathSegments + ), listRepoFiles: () => filterFileSet(listRepoFiles(repoRoot), skippedPathSegments), listPackageFiles: async (_packageName, packageRoot) => { const files = filterFileSet(await listRepoFiles(repoRoot), skippedPathSegments); @@ -47,15 +51,16 @@ export function createNodeValidationWorkspace(options: CreateNodeValidationWorks async function readWorkspaceFile( repoRoot: string, + gitPrefix: string, path: string, context?: ValidationWorkspaceReadContext ): Promise { const beforeRef = beforeStateTreeRef(repoRoot, context); - if (beforeRef !== undefined) return readTreeFile(repoRoot, beforeRef, path); + if (beforeRef !== undefined) return readTreeFile(repoRoot, gitPrefix, beforeRef, path); if (context?.scope.kind === "tree" && context.scope.treeRef !== undefined) { - return readTreeFile(repoRoot, context.scope.treeRef, path); + return readTreeFile(repoRoot, gitPrefix, context.scope.treeRef, path); } - if (context?.scope.kind === "staged") return readStagedFile(repoRoot, path); + if (context?.scope.kind === "staged") return readStagedFile(repoRoot, gitPrefix, path); return readDiskFile(repoRoot, path); } @@ -98,9 +103,9 @@ function hasSkippedSegment(path: string, skippedPathSegments: ReadonlySet skippedPathSegments.has(segment)); } -function readStagedFile(repoRoot: string, path: string): ValidationWorkspaceReadFileResult { +function readStagedFile(repoRoot: string, gitPrefix: string, path: string): ValidationWorkspaceReadFileResult { const normalized = validateRepoRelativePath(path); - const result = git(repoRoot, ["show", `:${normalized}`]); + const result = git(repoRoot, ["show", `:${gitRepoPath(gitPrefix, normalized)}`]); if (result.ok) return { status: "found", content: result.stdout }; if (/exists on disk, but not in|Path .* exists, but not|does not exist/.test(result.cause)) { return { status: "missing" }; @@ -111,9 +116,9 @@ function readStagedFile(repoRoot: string, path: string): ValidationWorkspaceRead function listChangedFiles(repoRoot: string, baseRef: string): ValidationWorkspaceFileSet { const base = resolveChangedBase(repoRoot, baseRef); if (!base.ok) return unavailable(base); - const diff = listDiffFiles(repoRoot, ["diff", "--name-status", "-z", "--find-renames", base.diffBase, "--"]); + const diff = listDiffFiles(repoRoot, ["diff", "--relative", "--name-status", "-z", "--find-renames", base.diffBase, "--", "."]); if (diff.unavailable) return diff; - const untracked = git(repoRoot, ["ls-files", "--others", "--exclude-standard", "-z"]); + const untracked = git(repoRoot, ["ls-files", "--others", "--exclude-standard", "-z", "--", "."]); if (!untracked.ok) return unavailable(untracked); return { files: [ @@ -131,7 +136,7 @@ function listTreeFiles(repoRoot: string, treeRef: string, changedFrom: string): if (!tree.ok) return unavailable(tree); const base = resolveTreeish(repoRoot, changedFrom); if (!base.ok) return unavailable(base); - return listDiffFiles(repoRoot, ["diff", "--name-status", "-z", "--find-renames", base.treeSha, tree.treeSha, "--"]); + return listDiffFiles(repoRoot, ["diff", "--relative", "--name-status", "-z", "--find-renames", base.treeSha, tree.treeSha, "--", "."]); } function listWorkspaceFiles( @@ -185,7 +190,7 @@ function listDiskFiles(repoRoot: string, skippedPathSegments: ReadonlySet { } }); + it("scopes changed files and base-tree reads to a nested Git workspace", async () => { + const temp = mkdtempSync(join(tmpdir(), "lattice-validation-nested-workspace-")); + try { + const appRoot = join(temp, "packages/app"); + mkdirSync(join(appRoot, "src"), { recursive: true }); + mkdirSync(join(temp, "packages/other/src"), { recursive: true }); + writeFileSync(join(appRoot, "src/index.ts"), "export const value = 'app-base';\n"); + writeFileSync(join(temp, "packages/other/src/index.ts"), "export const value = 'other-base';\n"); + const baseCommit = initializeGitSnapshot(temp, ["packages/app/src/index.ts", "packages/other/src/index.ts"]); + writeFileSync(join(appRoot, "src/index.ts"), "export const value = 'app-changed';\n"); + writeFileSync(join(appRoot, "src/new.ts"), "export const added = true;\n"); + writeFileSync(join(temp, "packages/other/src/index.ts"), "export const value = 'other-changed';\n"); + + const workspace = createNodeValidationWorkspace({ repoRoot: appRoot }); + const before = await workspace.readFile("src/index.ts", { + scope: { kind: "changed", baseRef: baseCommit, files: ["src/index.ts"] }, + state: "before" + }); + const changed = workspace.listChangedFiles(baseCommit); + const beforeFiles = workspace.listFiles({ + scope: { kind: "changed", baseRef: baseCommit, files: ["src/index.ts"] }, + state: "before" + }); + + assert.deepEqual(before, { status: "found", content: "export const value = 'app-base';\n" }); + assert.deepEqual(changed, { + files: [ + { path: "src/index.ts", status: "modified" }, + { path: "src/new.ts", status: "added" } + ] + }); + assert.deepEqual(beforeFiles, { files: ["src/index.ts", "src/new.ts"] }); + } finally { + rmSync(temp, { recursive: true, force: true }); + } + }); + it("defaults changed-scope validation to introduced report mode", async () => { const temp = mkdtempSync(join(tmpdir(), "lattice-validation-changed-default-introduced-")); try {