vcs.ts

  1import { existsSync } from 'fs';
  2import { join } from 'path';
  3import type { ChangeStack, FileChange, DiffMode, DiffScope } from '$lib/types/index';
  4import { getRepoDir } from './repo.ts';
  5import * as git from './git.ts';
  6import * as jj from './jj.ts';
  7
  8export { getRepoDir };
  9export { parseDiff, getLanguage } from './diff-parser.ts';
 10export type { DiffMode };
 11
 12export type VcsKind = 'git' | 'jj';
 13
 14let cachedKind: VcsKind | null = null;
 15
 16/**
 17 * Detect which VCS client to use. A `.jj/` directory wins over `.git` (jj repos
 18 * are often colocated with git), as long as the `jj` binary is actually
 19 * available. Falls back to git with a warning otherwise.
 20 */
 21export async function getVcsKind(): Promise<VcsKind> {
 22	if (cachedKind) return cachedKind;
 23
 24	const repoDir = getRepoDir();
 25	const hasJj = existsSync(join(repoDir, '.jj'));
 26	const hasGit = existsSync(join(repoDir, '.git'));
 27
 28	if (hasJj && (await jj.isJjAvailable())) {
 29		cachedKind = 'jj';
 30		return cachedKind;
 31	}
 32
 33	if (hasJj && !hasGit) {
 34		throw new Error(
 35			'Found a .jj directory but the `jj` CLI is not available on PATH. Install jujutsu to review this repository.'
 36		);
 37	}
 38
 39	if (hasJj) {
 40		console.warn('  Warning: .jj directory found but `jj` is unavailable; falling back to git.');
 41	}
 42
 43	cachedKind = 'git';
 44	return cachedKind;
 45}
 46
 47async function impl(): Promise<typeof git | typeof jj> {
 48	return (await getVcsKind()) === 'jj' ? jj : git;
 49}
 50
 51export async function getStatus(): Promise<FileChange[]> {
 52	return (await impl()).getStatus();
 53}
 54
 55export async function getWorktreeStatus(): Promise<FileChange[]> {
 56	return (await impl()).getWorktreeStatus();
 57}
 58
 59export async function getBaseBranchInfo(): Promise<{ baseBranch: string | null; mergeBase: string | null }> {
 60	return (await impl()).getBaseBranchInfo();
 61}
 62
 63export async function getOldRef(scope: DiffScope = 'uncommitted'): Promise<string> {
 64	return (await impl()).getOldRef(scope);
 65}
 66
 67export async function getNewRef(): Promise<string | null> {
 68	return (await impl()).getNewRef();
 69}
 70
 71export async function getChangeStack(): Promise<ChangeStack> {
 72	return (await impl()).getChangeStack();
 73}
 74
 75export async function selectChange(changeId: string): Promise<ChangeStack> {
 76	return (await impl()).selectChange(changeId);
 77}
 78
 79export async function approveChange(changeId?: string): Promise<ChangeStack> {
 80	return (await impl()).approveChange(changeId);
 81}
 82
 83export async function getDiffByMode(
 84	mode: DiffMode,
 85	filePath?: string,
 86	scope?: DiffScope
 87): Promise<string> {
 88	return (await impl()).getDiffByMode(mode, filePath, scope);
 89}
 90
 91export async function getFileContent(filePath: string, ref: string = 'HEAD'): Promise<string> {
 92	return (await impl()).getFileContent(filePath, ref);
 93}
 94
 95export async function approveFile(filePath: string, scope?: DiffScope): Promise<void> {
 96	return (await impl()).approveFile(filePath, scope);
 97}
 98
 99export async function unapproveFile(filePath: string, scope?: DiffScope): Promise<void> {
100	return (await impl()).unapproveFile(filePath, scope);
101}
102
103export async function resetFile(filePath: string): Promise<void> {
104	return (await impl()).resetFile(filePath);
105}
106
107export async function resetHunk(filePath: string, hunkHeader: string): Promise<void> {
108	return (await impl()).resetHunk(filePath, hunkHeader);
109}
110
111export async function commitStaged(message: string): Promise<string> {
112	return (await impl()).commitStaged(message);
113}
114
115export async function commitAll(message: string): Promise<string> {
116	return (await impl()).commitAll(message);
117}
118
119export async function getRepoInfo(): Promise<{ branch: string; repoName: string }> {
120	return (await impl()).getRepoInfo();
121}