-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathconnection.ts
More file actions
265 lines (247 loc) · 8.73 KB
/
Copy pathconnection.ts
File metadata and controls
265 lines (247 loc) · 8.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import Database from 'better-sqlite3';
import { debug, warn } from '../infrastructure/logger.js';
import { DbError } from '../shared/errors.js';
import type { BetterSqlite3Database } from '../types.js';
import { Repository } from './repository/base.js';
import { SqliteRepository } from './repository/sqlite-repository.js';
/** Lazy-loaded package version (read once from package.json). */
let _packageVersion: string | undefined;
function getPackageVersion(): string {
if (_packageVersion !== undefined) return _packageVersion;
try {
const connDir = path.dirname(fileURLToPath(import.meta.url));
const pkgPath = path.join(connDir, '..', '..', 'package.json');
_packageVersion = (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string })
.version;
} catch {
_packageVersion = '';
}
return _packageVersion;
}
/** Warn once per process when DB version mismatches the running codegraph version. */
let _versionWarned = false;
/** DB instance with optional advisory lock path. */
export type LockedDatabase = BetterSqlite3Database & { __lockPath?: string };
let _cachedRepoRoot: string | null | undefined; // undefined = not computed, null = not a git repo
let _cachedRepoRootCwd: string | undefined; // cwd at the time the cache was populated
/**
* Return the git worktree/repo root for the given directory (or cwd).
* Uses `git rev-parse --show-toplevel` which returns the correct root
* for both regular repos and git worktrees.
* Results are cached per-process when called without arguments.
* The cache is keyed on cwd so it invalidates if the working directory changes
* (e.g. MCP server serving multiple sessions).
*/
export function findRepoRoot(fromDir?: string): string | null {
const dir = fromDir || process.cwd();
if (!fromDir && _cachedRepoRoot !== undefined && _cachedRepoRootCwd === dir) {
return _cachedRepoRoot;
}
let root: string | null = null;
try {
const raw = execFileSync('git', ['rev-parse', '--show-toplevel'], {
cwd: dir,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
// Use realpathSync to resolve symlinks (macOS /var → /private/var) and
// 8.3 short names (Windows RUNNER~1 → runneradmin) so the ceiling path
// matches the realpathSync'd dir in findDbPath.
try {
root = fs.realpathSync(raw);
} catch (e) {
debug(`realpathSync failed for git root "${raw}", using resolve: ${(e as Error).message}`);
root = path.resolve(raw);
}
} catch (e) {
debug(`git rev-parse failed for "${dir}": ${(e as Error).message}`);
root = null;
}
if (!fromDir) {
_cachedRepoRoot = root;
_cachedRepoRootCwd = dir;
}
return root;
}
/** Reset the cached repo root (for testing). */
export function _resetRepoRootCache(): void {
_cachedRepoRoot = undefined;
_cachedRepoRootCwd = undefined;
}
/** Reset the version warning flag (for testing). */
export function _resetVersionWarning(): void {
_versionWarned = false;
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (e) {
debug(`PID ${pid} not alive: ${(e as NodeJS.ErrnoException).code || (e as Error).message}`);
return false;
}
}
function acquireAdvisoryLock(dbPath: string): void {
const lockPath = `${dbPath}.lock`;
try {
if (fs.existsSync(lockPath)) {
const content = fs.readFileSync(lockPath, 'utf-8').trim();
const pid = Number(content);
if (pid && pid !== process.pid && isProcessAlive(pid)) {
warn(`Another process (PID ${pid}) may be using this database. Proceeding with caution.`);
}
}
} catch (e) {
debug(`Advisory lock read failed: ${(e as Error).message}`);
}
try {
fs.writeFileSync(lockPath, String(process.pid), 'utf-8');
} catch (e) {
debug(`Advisory lock write failed: ${(e as Error).message}`);
}
}
function releaseAdvisoryLock(lockPath: string): void {
try {
const content = fs.readFileSync(lockPath, 'utf-8').trim();
if (Number(content) === process.pid) {
fs.unlinkSync(lockPath);
}
} catch (e) {
debug(`Advisory lock release failed for ${lockPath}: ${(e as Error).message}`);
}
}
/**
* Check if two paths refer to the same directory.
* Handles Windows 8.3 short names (RUNNER~1 vs runneradmin) and macOS
* symlinks (/tmp vs /private/tmp) where string comparison fails.
*/
function isSameDirectory(a: string, b: string): boolean {
if (path.resolve(a) === path.resolve(b)) return true;
try {
const sa = fs.statSync(a);
const sb = fs.statSync(b);
return sa.dev === sb.dev && sa.ino === sb.ino;
} catch (e) {
debug(`isSameDirectory stat failed: ${(e as Error).message}`);
return false;
}
}
export function openDb(dbPath: string): LockedDatabase {
const dir = path.dirname(dbPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
acquireAdvisoryLock(dbPath);
const db = new Database(dbPath) as unknown as LockedDatabase;
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
db.__lockPath = `${dbPath}.lock`;
return db;
}
export function closeDb(db: LockedDatabase): void {
db.close();
if (db.__lockPath) releaseAdvisoryLock(db.__lockPath);
}
export function findDbPath(customPath?: string): string {
if (customPath) return path.resolve(customPath);
const rawCeiling = findRepoRoot();
// Normalize ceiling with realpathSync to resolve 8.3 short names (Windows
// RUNNER~1 → runneradmin) and symlinks (macOS /var → /private/var).
// findRepoRoot already applies realpathSync internally, but the git output
// may still contain short names on some Windows CI environments.
let ceiling: string | null;
if (rawCeiling) {
try {
ceiling = fs.realpathSync(rawCeiling);
} catch (e) {
debug(`realpathSync failed for ceiling "${rawCeiling}": ${(e as Error).message}`);
ceiling = rawCeiling;
}
} else {
ceiling = null;
}
// Resolve symlinks (e.g. macOS /var → /private/var) so dir matches ceiling from git
let dir: string;
try {
dir = fs.realpathSync(process.cwd());
} catch (e) {
debug(`realpathSync failed for cwd: ${(e as Error).message}`);
dir = process.cwd();
}
while (true) {
const candidate = path.join(dir, '.codegraph', 'graph.db');
if (fs.existsSync(candidate)) return candidate;
if (ceiling && isSameDirectory(dir, ceiling)) {
debug(`findDbPath: stopped at git ceiling ${ceiling}`);
break;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
const base = ceiling || process.cwd();
return path.join(base, '.codegraph', 'graph.db');
}
/** Open a database in readonly mode, with a user-friendly error if the DB doesn't exist. */
export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database {
const dbPath = findDbPath(customPath);
if (!fs.existsSync(dbPath)) {
throw new DbError(
`No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
{ file: dbPath },
);
}
const db = new (
Database as unknown as new (
path: string,
opts?: Record<string, unknown>,
) => BetterSqlite3Database
)(dbPath, { readonly: true });
// Warn once per process if the DB was built with a different codegraph version
if (!_versionWarned) {
try {
const row = db
.prepare<{ value: string }>('SELECT value FROM build_meta WHERE key = ?')
.get('codegraph_version');
const buildVersion = row?.value;
const currentVersion = getPackageVersion();
if (buildVersion && currentVersion && buildVersion !== currentVersion) {
warn(
`DB was built with codegraph v${buildVersion}, running v${currentVersion}. Consider: codegraph build --no-incremental`,
);
}
} catch {
// build_meta table may not exist in older DBs — silently ignore
}
_versionWarned = true;
}
return db;
}
/**
* Open a Repository from either an injected instance or a DB path.
*
* When `opts.repo` is a Repository instance, returns it directly (no DB opened).
* Otherwise opens a readonly SQLite DB and wraps it in SqliteRepository.
*/
export function openRepo(
customDbPath?: string,
opts: { repo?: Repository } = {},
): { repo: Repository; close(): void } {
if (opts.repo != null) {
if (!(opts.repo instanceof Repository)) {
throw new TypeError(
`openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(opts.repo)}`,
);
}
return { repo: opts.repo, close() {} };
}
const db = openReadonlyOrFail(customDbPath);
return {
repo: new SqliteRepository(db),
close() {
db.close();
},
};
}