fix: treat git stage paths as literals
This commit is contained in:
parent
5f84e6d5b5
commit
350adb33ba
|
|
@ -0,0 +1,72 @@
|
|||
import { execFileSync } from 'child_process'
|
||||
import { mkdtemp, rm, writeFile } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import * as path from 'path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { bulkStageFiles, bulkUnstageFiles, stageFile, unstageFile } from './status'
|
||||
|
||||
const tempRoots: string[] = []
|
||||
|
||||
async function createRepoWithGlobNamedFiles(): Promise<string> {
|
||||
const repo = await mkdtemp(path.join(tmpdir(), 'orca-status-pathspec-'))
|
||||
tempRoots.push(repo)
|
||||
execFileSync('git', ['init', '-q'], { cwd: repo })
|
||||
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: repo })
|
||||
execFileSync('git', ['config', 'user.name', 'Test User'], { cwd: repo })
|
||||
await writeFile(path.join(repo, '*.log'), 'selected')
|
||||
await writeFile(path.join(repo, 'keep.log'), 'keep')
|
||||
execFileSync('git', ['add', '*.log', 'keep.log'], { cwd: repo })
|
||||
execFileSync('git', ['commit', '-q', '-m', 'initial'], { cwd: repo })
|
||||
await writeFile(path.join(repo, '*.log'), 'selected modified')
|
||||
await writeFile(path.join(repo, 'keep.log'), 'keep modified')
|
||||
return repo
|
||||
}
|
||||
|
||||
function gitNames(repo: string, args: string[]): string[] {
|
||||
const stdout = execFileSync('git', args, { cwd: repo, encoding: 'utf8' })
|
||||
return stdout.split(/\r?\n/).filter(Boolean)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
describe('git status pathspec literals', () => {
|
||||
it('stages a tracked path with Git glob characters as one literal path', async () => {
|
||||
const repo = await createRepoWithGlobNamedFiles()
|
||||
|
||||
await stageFile(repo, '*.log')
|
||||
|
||||
expect(gitNames(repo, ['diff', '--cached', '--name-only'])).toEqual(['*.log'])
|
||||
expect(gitNames(repo, ['diff', '--name-only'])).toEqual(['keep.log'])
|
||||
})
|
||||
|
||||
it('bulk stages tracked paths with Git glob characters as literal paths', async () => {
|
||||
const repo = await createRepoWithGlobNamedFiles()
|
||||
|
||||
await bulkStageFiles(repo, ['*.log'])
|
||||
|
||||
expect(gitNames(repo, ['diff', '--cached', '--name-only'])).toEqual(['*.log'])
|
||||
expect(gitNames(repo, ['diff', '--name-only'])).toEqual(['keep.log'])
|
||||
})
|
||||
|
||||
it('unstages a tracked path with Git glob characters as one literal path', async () => {
|
||||
const repo = await createRepoWithGlobNamedFiles()
|
||||
execFileSync('git', ['add', '*.log', 'keep.log'], { cwd: repo })
|
||||
|
||||
await unstageFile(repo, '*.log')
|
||||
|
||||
expect(gitNames(repo, ['diff', '--cached', '--name-only'])).toEqual(['keep.log'])
|
||||
expect(gitNames(repo, ['diff', '--name-only'])).toEqual(['*.log'])
|
||||
})
|
||||
|
||||
it('bulk unstages tracked paths with Git glob characters as literal paths', async () => {
|
||||
const repo = await createRepoWithGlobNamedFiles()
|
||||
execFileSync('git', ['add', '*.log', 'keep.log'], { cwd: repo })
|
||||
|
||||
await bulkUnstageFiles(repo, ['*.log'])
|
||||
|
||||
expect(gitNames(repo, ['diff', '--cached', '--name-only'])).toEqual(['keep.log'])
|
||||
expect(gitNames(repo, ['diff', '--name-only'])).toEqual(['*.log'])
|
||||
})
|
||||
})
|
||||
|
|
@ -139,14 +139,14 @@ describe('bulk git helpers', () => {
|
|||
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(3)
|
||||
expect(gitExecFileAsyncMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
['add', '--', ...filePaths.slice(0, 100)],
|
||||
['add', '--', ...filePaths.slice(0, 100).map((filePath) => `:(literal)${filePath}`)],
|
||||
{
|
||||
cwd: '/repo'
|
||||
}
|
||||
)
|
||||
expect(gitExecFileAsyncMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
['add', '--', ...filePaths.slice(200)],
|
||||
['add', '--', ...filePaths.slice(200).map((filePath) => `:(literal)${filePath}`)],
|
||||
{
|
||||
cwd: '/repo'
|
||||
}
|
||||
|
|
@ -162,7 +162,12 @@ describe('bulk git helpers', () => {
|
|||
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(2)
|
||||
expect(gitExecFileAsyncMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
['restore', '--staged', '--', ...filePaths.slice(100)],
|
||||
[
|
||||
'restore',
|
||||
'--staged',
|
||||
'--',
|
||||
...filePaths.slice(100).map((filePath) => `:(literal)${filePath}`)
|
||||
],
|
||||
{
|
||||
cwd: '/repo'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1020,14 +1020,16 @@ const PREVIEWABLE_BINARY_MIME_TYPES: Record<string, string> = {
|
|||
* Stage a file.
|
||||
*/
|
||||
export async function stageFile(worktreePath: string, filePath: string): Promise<void> {
|
||||
await gitExecFileAsync(['add', '--', filePath], { cwd: worktreePath })
|
||||
await gitExecFileAsync(['add', '--', literalPathspec(filePath)], { cwd: worktreePath })
|
||||
}
|
||||
|
||||
/**
|
||||
* Unstage a file.
|
||||
*/
|
||||
export async function unstageFile(worktreePath: string, filePath: string): Promise<void> {
|
||||
await gitExecFileAsync(['restore', '--staged', '--', filePath], { cwd: worktreePath })
|
||||
await gitExecFileAsync(['restore', '--staged', '--', literalPathspec(filePath)], {
|
||||
cwd: worktreePath
|
||||
})
|
||||
}
|
||||
|
||||
export async function getStagedCommitContext(
|
||||
|
|
@ -1246,7 +1248,7 @@ export async function bulkStageFiles(worktreePath: string, filePaths: string[]):
|
|||
}
|
||||
for (let i = 0; i < filePaths.length; i += BULK_CHUNK_SIZE) {
|
||||
const chunk = filePaths.slice(i, i + BULK_CHUNK_SIZE)
|
||||
await gitExecFileAsync(['add', '--', ...chunk], { cwd: worktreePath })
|
||||
await gitExecFileAsync(['add', '--', ...chunk.map(literalPathspec)], { cwd: worktreePath })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1259,6 +1261,8 @@ export async function bulkUnstageFiles(worktreePath: string, filePaths: string[]
|
|||
}
|
||||
for (let i = 0; i < filePaths.length; i += BULK_CHUNK_SIZE) {
|
||||
const chunk = filePaths.slice(i, i + BULK_CHUNK_SIZE)
|
||||
await gitExecFileAsync(['restore', '--staged', '--', ...chunk], { cwd: worktreePath })
|
||||
await gitExecFileAsync(['restore', '--staged', '--', ...chunk.map(literalPathspec)], {
|
||||
cwd: worktreePath
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue