fix(glob): default hidden=true and follow=true to align with OpenCode (#720)
- Add follow?: boolean option to GlobOptions interface - Change buildRgArgs to use !== false pattern for hidden and follow flags - Change buildFindArgs to use === false pattern, add -L for symlinks - Change buildPowerShellCommand to use !== false pattern for hidden - Remove -FollowSymlink from PowerShell (unsupported in PS 5.1) - Export build functions for testing - Add comprehensive BDD-style tests (18 tests, 21 assertions) Note: Symlink following via -FollowSymlink is not supported in Windows PowerShell 5.1. OpenCode auto-downloads ripgrep which handles symlinks via --follow flag. PowerShell fallback is a safety net that rarely triggers. Fixes #631
This commit is contained in:
parent
e620b546ab
commit
2314a0d371
158
src/tools/glob/cli.test.ts
Normal file
158
src/tools/glob/cli.test.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { describe, it, expect } from "bun:test"
|
||||||
|
import { buildRgArgs, buildFindArgs, buildPowerShellCommand } from "./cli"
|
||||||
|
|
||||||
|
describe("buildRgArgs", () => {
|
||||||
|
// #given default options (no hidden/follow specified)
|
||||||
|
// #when building ripgrep args
|
||||||
|
// #then should include --hidden and --follow by default
|
||||||
|
it("includes --hidden by default when not explicitly set", () => {
|
||||||
|
const args = buildRgArgs({ pattern: "*.ts" })
|
||||||
|
expect(args).toContain("--hidden")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes --follow by default when not explicitly set", () => {
|
||||||
|
const args = buildRgArgs({ pattern: "*.ts" })
|
||||||
|
expect(args).toContain("--follow")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given hidden=false explicitly set
|
||||||
|
// #when building ripgrep args
|
||||||
|
// #then should NOT include --hidden
|
||||||
|
it("excludes --hidden when explicitly set to false", () => {
|
||||||
|
const args = buildRgArgs({ pattern: "*.ts", hidden: false })
|
||||||
|
expect(args).not.toContain("--hidden")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given follow=false explicitly set
|
||||||
|
// #when building ripgrep args
|
||||||
|
// #then should NOT include --follow
|
||||||
|
it("excludes --follow when explicitly set to false", () => {
|
||||||
|
const args = buildRgArgs({ pattern: "*.ts", follow: false })
|
||||||
|
expect(args).not.toContain("--follow")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given hidden=true explicitly set
|
||||||
|
// #when building ripgrep args
|
||||||
|
// #then should include --hidden
|
||||||
|
it("includes --hidden when explicitly set to true", () => {
|
||||||
|
const args = buildRgArgs({ pattern: "*.ts", hidden: true })
|
||||||
|
expect(args).toContain("--hidden")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given follow=true explicitly set
|
||||||
|
// #when building ripgrep args
|
||||||
|
// #then should include --follow
|
||||||
|
it("includes --follow when explicitly set to true", () => {
|
||||||
|
const args = buildRgArgs({ pattern: "*.ts", follow: true })
|
||||||
|
expect(args).toContain("--follow")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given pattern with special characters
|
||||||
|
// #when building ripgrep args
|
||||||
|
// #then should include glob pattern correctly
|
||||||
|
it("includes the glob pattern", () => {
|
||||||
|
const args = buildRgArgs({ pattern: "**/*.tsx" })
|
||||||
|
expect(args).toContain("--glob=**/*.tsx")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildFindArgs", () => {
|
||||||
|
// #given default options (no hidden/follow specified)
|
||||||
|
// #when building find args
|
||||||
|
// #then should include hidden files by default (no exclusion filter)
|
||||||
|
it("includes hidden files by default when not explicitly set", () => {
|
||||||
|
const args = buildFindArgs({ pattern: "*.ts" })
|
||||||
|
// When hidden is enabled (default), should NOT have the exclusion filter
|
||||||
|
expect(args).not.toContain("-not")
|
||||||
|
expect(args.join(" ")).not.toContain("*/.*")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given default options (no follow specified)
|
||||||
|
// #when building find args
|
||||||
|
// #then should include -L flag for symlink following by default
|
||||||
|
it("includes -L flag for symlink following by default", () => {
|
||||||
|
const args = buildFindArgs({ pattern: "*.ts" })
|
||||||
|
expect(args).toContain("-L")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given hidden=false explicitly set
|
||||||
|
// #when building find args
|
||||||
|
// #then should exclude hidden files
|
||||||
|
it("excludes hidden files when hidden is explicitly false", () => {
|
||||||
|
const args = buildFindArgs({ pattern: "*.ts", hidden: false })
|
||||||
|
expect(args).toContain("-not")
|
||||||
|
expect(args.join(" ")).toContain("*/.*")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given follow=false explicitly set
|
||||||
|
// #when building find args
|
||||||
|
// #then should NOT include -L flag
|
||||||
|
it("excludes -L flag when follow is explicitly false", () => {
|
||||||
|
const args = buildFindArgs({ pattern: "*.ts", follow: false })
|
||||||
|
expect(args).not.toContain("-L")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given hidden=true explicitly set
|
||||||
|
// #when building find args
|
||||||
|
// #then should include hidden files
|
||||||
|
it("includes hidden files when hidden is explicitly true", () => {
|
||||||
|
const args = buildFindArgs({ pattern: "*.ts", hidden: true })
|
||||||
|
expect(args).not.toContain("-not")
|
||||||
|
expect(args.join(" ")).not.toContain("*/.*")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given follow=true explicitly set
|
||||||
|
// #when building find args
|
||||||
|
// #then should include -L flag
|
||||||
|
it("includes -L flag when follow is explicitly true", () => {
|
||||||
|
const args = buildFindArgs({ pattern: "*.ts", follow: true })
|
||||||
|
expect(args).toContain("-L")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildPowerShellCommand", () => {
|
||||||
|
// #given default options (no hidden specified)
|
||||||
|
// #when building PowerShell command
|
||||||
|
// #then should include -Force by default
|
||||||
|
it("includes -Force by default when not explicitly set", () => {
|
||||||
|
const args = buildPowerShellCommand({ pattern: "*.ts" })
|
||||||
|
const command = args.join(" ")
|
||||||
|
expect(command).toContain("-Force")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given hidden=false explicitly set
|
||||||
|
// #when building PowerShell command
|
||||||
|
// #then should NOT include -Force
|
||||||
|
it("excludes -Force when hidden is explicitly false", () => {
|
||||||
|
const args = buildPowerShellCommand({ pattern: "*.ts", hidden: false })
|
||||||
|
const command = args.join(" ")
|
||||||
|
expect(command).not.toContain("-Force")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given hidden=true explicitly set
|
||||||
|
// #when building PowerShell command
|
||||||
|
// #then should include -Force
|
||||||
|
it("includes -Force when hidden is explicitly true", () => {
|
||||||
|
const args = buildPowerShellCommand({ pattern: "*.ts", hidden: true })
|
||||||
|
const command = args.join(" ")
|
||||||
|
expect(command).toContain("-Force")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given default options (no follow specified)
|
||||||
|
// #when building PowerShell command
|
||||||
|
// #then should NOT include -FollowSymlink (unsupported in Windows PowerShell 5.1)
|
||||||
|
it("does NOT include -FollowSymlink (unsupported in Windows PowerShell 5.1)", () => {
|
||||||
|
const args = buildPowerShellCommand({ pattern: "*.ts" })
|
||||||
|
const command = args.join(" ")
|
||||||
|
expect(command).not.toContain("-FollowSymlink")
|
||||||
|
})
|
||||||
|
|
||||||
|
// #given pattern with special chars
|
||||||
|
// #when building PowerShell command
|
||||||
|
// #then should escape single quotes properly
|
||||||
|
it("escapes single quotes in pattern", () => {
|
||||||
|
const args = buildPowerShellCommand({ pattern: "test's.ts" })
|
||||||
|
const command = args.join(" ")
|
||||||
|
expect(command).toContain("test''s.ts")
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -22,7 +22,8 @@ function buildRgArgs(options: GlobOptions): string[] {
|
|||||||
`--max-depth=${Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)}`,
|
`--max-depth=${Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)}`,
|
||||||
]
|
]
|
||||||
|
|
||||||
if (options.hidden) args.push("--hidden")
|
if (options.hidden !== false) args.push("--hidden")
|
||||||
|
if (options.follow !== false) args.push("--follow")
|
||||||
if (options.noIgnore) args.push("--no-ignore")
|
if (options.noIgnore) args.push("--no-ignore")
|
||||||
|
|
||||||
args.push(`--glob=${options.pattern}`)
|
args.push(`--glob=${options.pattern}`)
|
||||||
@ -31,7 +32,13 @@ function buildRgArgs(options: GlobOptions): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildFindArgs(options: GlobOptions): string[] {
|
function buildFindArgs(options: GlobOptions): string[] {
|
||||||
const args: string[] = ["."]
|
const args: string[] = []
|
||||||
|
|
||||||
|
if (options.follow !== false) {
|
||||||
|
args.push("-L")
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(".")
|
||||||
|
|
||||||
const maxDepth = Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)
|
const maxDepth = Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)
|
||||||
args.push("-maxdepth", String(maxDepth))
|
args.push("-maxdepth", String(maxDepth))
|
||||||
@ -39,7 +46,7 @@ function buildFindArgs(options: GlobOptions): string[] {
|
|||||||
args.push("-type", "f")
|
args.push("-type", "f")
|
||||||
args.push("-name", options.pattern)
|
args.push("-name", options.pattern)
|
||||||
|
|
||||||
if (!options.hidden) {
|
if (options.hidden === false) {
|
||||||
args.push("-not", "-path", "*/.*")
|
args.push("-not", "-path", "*/.*")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,10 +63,15 @@ function buildPowerShellCommand(options: GlobOptions): string[] {
|
|||||||
|
|
||||||
let psCommand = `Get-ChildItem -Path '${escapedPath}' -File -Recurse -Depth ${maxDepth - 1} -Filter '${escapedPattern}'`
|
let psCommand = `Get-ChildItem -Path '${escapedPath}' -File -Recurse -Depth ${maxDepth - 1} -Filter '${escapedPattern}'`
|
||||||
|
|
||||||
if (options.hidden) {
|
if (options.hidden !== false) {
|
||||||
psCommand += " -Force"
|
psCommand += " -Force"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Symlink following (-FollowSymlink) is NOT supported in PowerShell backend.
|
||||||
|
// -FollowSymlink was introduced in PowerShell Core 6.0+ and is unavailable in
|
||||||
|
// Windows PowerShell 5.1 (default on Windows). OpenCode auto-downloads ripgrep
|
||||||
|
// which handles symlinks via --follow. This fallback rarely triggers in practice.
|
||||||
|
|
||||||
psCommand += " -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName"
|
psCommand += " -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName"
|
||||||
|
|
||||||
return ["powershell", "-NoProfile", "-Command", psCommand]
|
return ["powershell", "-NoProfile", "-Command", psCommand]
|
||||||
@ -74,6 +86,8 @@ async function getFileMtime(filePath: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { buildRgArgs, buildFindArgs, buildPowerShellCommand }
|
||||||
|
|
||||||
export async function runRgFiles(
|
export async function runRgFiles(
|
||||||
options: GlobOptions,
|
options: GlobOptions,
|
||||||
resolvedCli?: ResolvedCli
|
resolvedCli?: ResolvedCli
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export interface GlobOptions {
|
|||||||
pattern: string
|
pattern: string
|
||||||
paths?: string[]
|
paths?: string[]
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
|
follow?: boolean
|
||||||
noIgnore?: boolean
|
noIgnore?: boolean
|
||||||
maxDepth?: number
|
maxDepth?: number
|
||||||
timeout?: number
|
timeout?: number
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user