Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions packages/config/src/options/repository_root.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { dirname } from 'path'
import { lstat } from 'fs/promises'
import { dirname, join } from 'path'

import { findUp } from 'find-up'

// Find out repository root among (in priority order):
// - `repositoryRoot` option
// - find a `.git` directory up from `cwd`
// - find a `.git` directory or file up from `cwd`
// - `cwd` (fallback)
// Git worktrees use a `.git` file (not a directory). Walk once and accept
// whichever marker is nearest so a parent `.git` directory cannot hide a
// closer worktree `.git` file.
const gitMarkerIn = async (directory) => {
const gitPath = join(directory, '.git')
try {
await lstat(gitPath)
return gitPath
} catch {
return undefined
}
}

export const getRepositoryRoot = async function ({ repositoryRoot, cwd }) {
if (repositoryRoot !== undefined) {
return repositoryRoot
}

const repositoryRootA = await findUp('.git', { cwd, type: 'directory' })
const repositoryRootA = await findUp(gitMarkerIn, { cwd })

if (repositoryRootA === undefined) {
return cwd
Expand Down
35 changes: 34 additions & 1 deletion packages/config/tests/cwd/cwd.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { relative } from 'path'
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join, relative, resolve } from 'path'
import { cwd } from 'process'
import { fileURLToPath } from 'url'

import { Fixture, normalizeOutput } from '@netlify/testing'
import { expect, test } from 'vitest'

import { getRepositoryRoot } from '../../src/options/repository_root.js'

const FIXTURES_DIR = fileURLToPath(new URL('fixtures', import.meta.url))

test('--cwd with no config', async () => {
Expand Down Expand Up @@ -50,6 +54,35 @@ test('No .git', async () => {
expect(normalizeOutput(output)).toMatchSnapshot()
})

test('git worktree .git file is treated as repository root', async () => {
const root = await mkdtemp(join(tmpdir(), 'netlify-config-worktree-'))
try {
await writeFile(join(root, '.git'), 'gitdir: /tmp/main/.git/worktrees/wt\n')
const nested = join(root, 'apps', 'site')
await mkdir(nested, { recursive: true })
const repositoryRoot = await getRepositoryRoot({ cwd: nested })
expect(resolve(repositoryRoot)).toBe(resolve(root))
} finally {
await rm(root, { recursive: true, force: true })
}
})

test('nearest worktree .git file wins over a parent .git directory', async () => {
const parent = await mkdtemp(join(tmpdir(), 'netlify-config-nested-wt-'))
try {
await mkdir(join(parent, '.git'))
const worktree = join(parent, 'wt')
await mkdir(worktree)
await writeFile(join(worktree, '.git'), 'gitdir: /tmp/main/.git/worktrees/wt\n')
const nested = join(worktree, 'apps', 'site')
await mkdir(nested, { recursive: true })
const repositoryRoot = await getRepositoryRoot({ cwd: nested })
expect(resolve(repositoryRoot)).toBe(resolve(worktree))
} finally {
await rm(parent, { recursive: true, force: true })
}
})

test('--cwd non-existing', async () => {
const output = await new Fixture()
.withFlags({ cwd: '/invalid', repositoryRoot: `${FIXTURES_DIR}/empty` })
Expand Down