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}