Files
hapi/cli/src/utils/worktreeEnv.ts
T

126 lines
3.7 KiB
TypeScript

import { execFileSync } from 'node:child_process';
import { realpathSync, statSync } from 'node:fs';
import { basename, dirname, isAbsolute, resolve } from 'node:path';
import type { WorktreeInfo } from '@/runner/worktree';
import { logger } from '@/ui/logger';
import { getInvokedCwd } from '@/utils/invokedCwd';
export function readWorktreeEnv(): WorktreeInfo | null {
return readWorktreeFromEnv() ?? readWorktreeFromGit();
}
function readWorktreeFromEnv(): WorktreeInfo | null {
const basePath = process.env.HAPI_WORKTREE_BASE_PATH?.trim();
const branch = process.env.HAPI_WORKTREE_BRANCH?.trim();
const name = process.env.HAPI_WORKTREE_NAME?.trim();
const worktreePath = process.env.HAPI_WORKTREE_PATH?.trim();
const createdAtRaw = process.env.HAPI_WORKTREE_CREATED_AT?.trim();
if (!basePath || !branch || !name || !worktreePath || !createdAtRaw) {
return null;
}
const createdAt = Number(createdAtRaw);
if (!Number.isFinite(createdAt)) {
return null;
}
return {
basePath,
branch,
name,
worktreePath,
createdAt
};
}
function readWorktreeFromGit(): WorktreeInfo | null {
const start = Date.now();
let result: WorktreeInfo | null = null;
try {
const cwd = getInvokedCwd();
const isInside = runGit(['rev-parse', '--is-inside-work-tree'], cwd);
if (isInside !== 'true') {
return null;
}
const gitDir = runGit(['rev-parse', '--git-dir'], cwd);
const gitCommonDir = runGit(['rev-parse', '--git-common-dir'], cwd);
if (!gitDir || !gitCommonDir) {
return null;
}
const resolvedGitDir = normalizePath(gitDir, cwd);
const resolvedGitCommonDir = normalizePath(gitCommonDir, cwd);
if (resolvedGitDir === resolvedGitCommonDir) {
return null;
}
const worktreeRoot = runGit(['rev-parse', '--show-toplevel'], cwd);
if (!worktreeRoot) {
return null;
}
const worktreePath = normalizePath(worktreeRoot, cwd);
const basePath = dirname(resolvedGitCommonDir);
const branch = runGit(['symbolic-ref', '--short', 'HEAD'], cwd)
?? runGit(['rev-parse', '--short', 'HEAD'], cwd);
if (!branch) {
return null;
}
result = {
basePath,
branch,
name: basename(worktreePath),
worktreePath,
createdAt: readCreatedAt(worktreePath)
};
return result;
} finally {
const elapsedMs = Date.now() - start;
logger.debug(`[WORKTREE] Git probe ${result ? 'hit' : 'miss'} in ${elapsedMs}ms`);
}
}
function runGit(args: string[], cwd: string): string | null {
try {
const output = execFileSync('git', args, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim();
return output.length > 0 ? output : null;
} catch {
return null;
}
}
function normalizePath(rawPath: string, cwd: string): string {
const resolved = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
try {
return realpathSync(resolved);
} catch {
return resolved;
}
}
function readCreatedAt(worktreePath: string): number {
try {
const stat = statSync(worktreePath);
const birthtimeMs = Math.round(stat.birthtimeMs);
if (Number.isFinite(birthtimeMs) && birthtimeMs > 0) {
return birthtimeMs;
}
const ctimeMs = Math.round(stat.ctimeMs);
if (Number.isFinite(ctimeMs) && ctimeMs > 0) {
return ctimeMs;
}
} catch {
return Date.now();
}
return Date.now();
}