From 18ed7b27dacf43f89757c807e5b0d5be8dd83cdc Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 01:30:35 -0700 Subject: [PATCH] fix: validate git repo detection --- src/main/git/repo-detection.test.ts | 37 +++++++++++++++++++++++++++++ src/main/git/repo.ts | 32 ++++++++++++------------- 2 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 src/main/git/repo-detection.test.ts diff --git a/src/main/git/repo-detection.test.ts b/src/main/git/repo-detection.test.ts new file mode 100644 index 00000000000..67597384782 --- /dev/null +++ b/src/main/git/repo-detection.test.ts @@ -0,0 +1,37 @@ +import { execFileSync } from 'child_process' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import * as path from 'path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { isGitRepo } from './repo' + +function git(cwd: string, args: string[]): string { + return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }) +} + +describe('isGitRepo', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = mkdtempSync(path.join(tmpdir(), 'orca-repo-detect-')) + }) + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }) + }) + + it('rejects directories with an invalid .git file', () => { + const fakeRepo = path.join(tmpDir, 'fake') + mkdirSync(fakeRepo) + writeFileSync(path.join(fakeRepo, '.git'), 'not a gitdir file') + + expect(isGitRepo(fakeRepo)).toBe(false) + }) + + it('accepts bare git repositories', () => { + const bareRepo = path.join(tmpDir, 'bare.git') + git(tmpDir, ['init', '--bare', '--quiet', bareRepo]) + + expect(isGitRepo(bareRepo)).toBe(true) + }) +}) diff --git a/src/main/git/repo.ts b/src/main/git/repo.ts index ddf56073eef..469f3e0d1a4 100644 --- a/src/main/git/repo.ts +++ b/src/main/git/repo.ts @@ -1,7 +1,7 @@ /* oxlint-disable max-lines */ import { execSync } from 'child_process' import { existsSync, statSync } from 'fs' -import { join, basename } from 'path' +import { basename } from 'path' import { gitExecFileSync, gitExecFileAsync } from './runner' import type { BaseRefSearchResult } from '../../shared/types' import { buildHostedRemoteFileUrl, parseHostedRemote } from './hosted-remote-url' @@ -53,25 +53,23 @@ export function isGitRepo(path: string): boolean { if (!existsSync(path) || !statSync(path).isDirectory()) { return false } - // .git dir or file (for worktrees) or bare repo - if (existsSync(join(path, '.git'))) { - return true - } - // Might be a bare repo — ask git - const result = gitExecFileSync(['rev-parse', '--is-inside-work-tree'], { + const insideWorkTree = gitExecFileSync(['rev-parse', '--is-inside-work-tree'], { cwd: path }).trim() - return result === 'true' - } catch { - // Also check if it's a bare repo - try { - const result = gitExecFileSync(['rev-parse', '--is-bare-repository'], { - cwd: path - }).trim() - return result === 'true' - } catch { - return false + if (insideWorkTree === 'true') { + return true } + } catch { + // Fall through to the bare-repo probe below. + } + + try { + const bareRepo = gitExecFileSync(['rev-parse', '--is-bare-repository'], { + cwd: path + }).trim() + return bareRepo === 'true' + } catch { + return false } }