gent-cli 14.0.0 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +7 -4
- package/src/commands/canonical.js +242 -0
- package/src/commands/clone.js +2 -13
- package/src/commands/pet.js +122 -98
- package/src/commands/pull.js +3 -4
- package/src/commands/push.js +4 -5
- package/src/commands/share.js +40 -15
- package/src/commands/web.js +30 -15
- package/src/index.js +70 -40
- package/src/utils/api-client.js +6 -9
- package/src/utils/attributes.js +280 -0
- package/src/utils/canonical-journal.js +122 -0
- package/src/utils/feature-support.js +92 -0
- package/src/utils/feature-support.json +210 -0
- package/src/utils/gent-ops.js +836 -0
- package/src/utils/git-config.js +606 -0
- package/src/utils/git-index.js +581 -0
- package/src/utils/git-objects.js +537 -0
- package/src/utils/ignore.js +365 -0
- package/src/utils/lockfile.js +219 -0
- package/src/utils/merge-ops.js +282 -0
- package/src/utils/migrate.js +257 -0
- package/src/utils/object-store.js +332 -36
- package/src/utils/packfile.js +812 -0
- package/src/utils/refs.js +595 -0
- package/src/utils/repository.js +533 -0
- package/src/utils/smart-http.js +195 -0
- package/src/utils/stash-ops.js +355 -0
- package/src/utils/user-config.js +10 -0
- package/src/utils/web-urls.js +125 -0
- package/src/utils/worktree.js +676 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Repository - discovery, validation and the handle everything else uses
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Find the repository from any directory, decide whether Gent can safely
|
|
8
|
+
* operate on it, and hand back one object carrying the object store, refs,
|
|
9
|
+
* config and paths.
|
|
10
|
+
*
|
|
11
|
+
* LAYOUTS UNDERSTOOD:
|
|
12
|
+
* <root>/.gent/ Gent-created; a .git *file* points at it
|
|
13
|
+
* <root>/.git/ ordinary Git clone
|
|
14
|
+
* <root>/.git a gitfile: "gitdir: <path>"
|
|
15
|
+
* bare repositories (core.bare = true)
|
|
16
|
+
* linked worktrees (gitdir + commondir files)
|
|
17
|
+
*
|
|
18
|
+
* VALIDATION HAPPENS BEFORE ANY WRITE:
|
|
19
|
+
* repositoryformatversion, unknown required extensions, the object format,
|
|
20
|
+
* the ref backend and any in-progress external Git operation are all checked
|
|
21
|
+
* at open time. An unsupported repository is refused intact.
|
|
22
|
+
*
|
|
23
|
+
* BRANDING IS NOT VALIDITY:
|
|
24
|
+
* The directory being called `.gent` never makes a repository valid, and its
|
|
25
|
+
* being called `.git` never makes one invalid.
|
|
26
|
+
* ============================================================================
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs').promises;
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
|
|
33
|
+
const { ObjectStore } = require('./object-store');
|
|
34
|
+
const { PackStore } = require('./packfile');
|
|
35
|
+
const { RefStore } = require('./refs');
|
|
36
|
+
const { ConfigFile, ConfigSet, expandIncludes } = require('./git-config');
|
|
37
|
+
const { UnsupportedFeatureError, feature, FORMAT_MARKER, OBJECT_FORMAT, REPOSITORY_FORMAT_VERSION } = require('./feature-support');
|
|
38
|
+
const { readFileOrNull, writeAtomic } = require('./lockfile');
|
|
39
|
+
const { timezoneOffset } = require('./git-objects');
|
|
40
|
+
|
|
41
|
+
const GENT_DIR = '.gent';
|
|
42
|
+
const GIT_DIR = '.git';
|
|
43
|
+
|
|
44
|
+
/** Extensions Gent understands. Anything else refuses the repository. */
|
|
45
|
+
const KNOWN_EXTENSIONS = new Set(['objectformat', 'worktreeconfig', 'preciousobjects', 'compatobjectformat']);
|
|
46
|
+
|
|
47
|
+
/** Marker files that mean another tool is mid-operation. */
|
|
48
|
+
const IN_PROGRESS_MARKERS = [
|
|
49
|
+
['rebase-merge', 'an interactive rebase'],
|
|
50
|
+
['rebase-apply', 'a rebase or "git am"'],
|
|
51
|
+
['CHERRY_PICK_HEAD', 'a cherry-pick'],
|
|
52
|
+
['REVERT_HEAD', 'a revert'],
|
|
53
|
+
['BISECT_LOG', 'a bisect'],
|
|
54
|
+
['sequencer', 'a sequencer operation']
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
class RepositoryError extends Error {
|
|
58
|
+
constructor(message, code) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = 'RepositoryError';
|
|
61
|
+
this.code = code || 'GENT_NOT_A_REPOSITORY';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A pre-v13 repository: JSON history, no canonical config. */
|
|
66
|
+
class LegacyRepositoryError extends Error {
|
|
67
|
+
constructor(gentPath) {
|
|
68
|
+
super(
|
|
69
|
+
`'${gentPath}' is a Gent v12 repository.\n` +
|
|
70
|
+
`Its history is stored in commits.json with randomly generated commit ids, which v13 cannot read.\n` +
|
|
71
|
+
`Run "gent migrate --dry-run" to see exactly what conversion would do; nothing is modified until you run "gent migrate".`
|
|
72
|
+
);
|
|
73
|
+
this.name = 'LegacyRepositoryError';
|
|
74
|
+
this.code = 'GENT_LEGACY_REPOSITORY';
|
|
75
|
+
this.gentPath = gentPath;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class Repository {
|
|
80
|
+
/**
|
|
81
|
+
* @param {Object} parts
|
|
82
|
+
*/
|
|
83
|
+
constructor(parts) {
|
|
84
|
+
/** @type {String} the per-worktree git directory */
|
|
85
|
+
this.gitdir = parts.gitdir;
|
|
86
|
+
/** @type {String} shared objects/refs directory (differs in linked worktrees) */
|
|
87
|
+
this.commondir = parts.commondir;
|
|
88
|
+
/** @type {String|null} null for a bare repository */
|
|
89
|
+
this.worktree = parts.worktree;
|
|
90
|
+
/** @type {Boolean} */
|
|
91
|
+
this.bare = parts.bare;
|
|
92
|
+
/** @type {ConfigSet} */
|
|
93
|
+
this.config = parts.config;
|
|
94
|
+
/** @type {ConfigFile} the file `gent config` writes to */
|
|
95
|
+
this.localConfig = parts.localConfig;
|
|
96
|
+
/** @type {Boolean} true when the gitdir is named .gent */
|
|
97
|
+
this.gentBranded = path.basename(this.commondir) === GENT_DIR;
|
|
98
|
+
|
|
99
|
+
const objectsDir = path.join(this.commondir, 'objects');
|
|
100
|
+
this.objects = new ObjectStore(objectsDir, { packBackend: new PackStore(objectsDir) });
|
|
101
|
+
this.refs = new RefStore(this);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Gent's own metadata namespace inside the resolved gitdir. */
|
|
105
|
+
get gentMetaDir() {
|
|
106
|
+
return path.join(this.commondir, 'gent');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Per-worktree Gent metadata (operation state that must not be shared). */
|
|
110
|
+
get gentWorktreeMetaDir() {
|
|
111
|
+
return path.join(this.gitdir, 'gent');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get indexPath() {
|
|
115
|
+
return path.join(this.gitdir, 'index');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {String} name
|
|
120
|
+
* @returns {String}
|
|
121
|
+
*/
|
|
122
|
+
gitPath(name) {
|
|
123
|
+
return path.join(this.gitdir, name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {String} name
|
|
128
|
+
* @returns {String}
|
|
129
|
+
*/
|
|
130
|
+
commonPath(name) {
|
|
131
|
+
return path.join(this.commondir, name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Committer/author identity from config, or null when unset.
|
|
136
|
+
* @param {String} [kind] - 'author' | 'committer'
|
|
137
|
+
* @returns {Promise<{name, email, timestamp, timezone}|null>}
|
|
138
|
+
*/
|
|
139
|
+
async identity(kind = 'committer') {
|
|
140
|
+
const now = new Date();
|
|
141
|
+
const name = process.env[`GIT_${kind.toUpperCase()}_NAME`] || this.config.get('user.name');
|
|
142
|
+
const email = process.env[`GIT_${kind.toUpperCase()}_EMAIL`] || this.config.get('user.email');
|
|
143
|
+
if (!name || !email) return null;
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
email,
|
|
147
|
+
timestamp: Math.floor(now.getTime() / 1000),
|
|
148
|
+
timezone: timezoneOffset(now)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Used by RefStore for reflog lines; absent identity means no reflog. */
|
|
153
|
+
async reflogIdentity() {
|
|
154
|
+
return this.identity('committer');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Refuse to write while another tool holds the repository mid-operation.
|
|
159
|
+
* @param {String} what - the Gent operation being attempted
|
|
160
|
+
*/
|
|
161
|
+
async assertNoExternalOperation(what) {
|
|
162
|
+
for (const [marker, description] of IN_PROGRESS_MARKERS) {
|
|
163
|
+
for (const candidate of [this.gitPath(marker), this.commonPath(marker)]) {
|
|
164
|
+
const present = await fs.access(candidate).then(() => true, () => false);
|
|
165
|
+
if (!present) continue;
|
|
166
|
+
throw new UnsupportedFeatureError(
|
|
167
|
+
[{ ...feature('sequencer.external'), detail: `${description} is in progress (${path.basename(candidate)} exists)` }],
|
|
168
|
+
what
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @returns {Promise<String>} absolute path
|
|
176
|
+
* @throws when the repository is bare
|
|
177
|
+
*/
|
|
178
|
+
requireWorktree(what) {
|
|
179
|
+
if (!this.worktree) {
|
|
180
|
+
throw new RepositoryError(`${what} requires a working tree, and this is a bare repository`, 'GENT_BARE_REPOSITORY');
|
|
181
|
+
}
|
|
182
|
+
return this.worktree;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Path of a worktree file relative to the worktree root, in POSIX form.
|
|
187
|
+
* @param {String} absolutePath
|
|
188
|
+
* @returns {String}
|
|
189
|
+
*/
|
|
190
|
+
relativePath(absolutePath) {
|
|
191
|
+
const root = this.requireWorktree('resolving a path');
|
|
192
|
+
const relative = path.relative(root, path.resolve(absolutePath));
|
|
193
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
194
|
+
throw new RepositoryError(`'${absolutePath}' is outside the repository at ${root}`, 'GENT_OUTSIDE_WORKTREE');
|
|
195
|
+
}
|
|
196
|
+
return relative.split(path.sep).join('/');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read a gitfile ("gitdir: <path>") and resolve it.
|
|
202
|
+
* @param {String} filePath
|
|
203
|
+
* @returns {Promise<String|null>}
|
|
204
|
+
*/
|
|
205
|
+
async function readGitfile(filePath) {
|
|
206
|
+
const raw = await readFileOrNull(filePath);
|
|
207
|
+
if (!raw) return null;
|
|
208
|
+
|
|
209
|
+
const text = raw.toString('utf-8').trim();
|
|
210
|
+
if (!text.startsWith('gitdir:')) return null;
|
|
211
|
+
|
|
212
|
+
const target = text.slice('gitdir:'.length).trim();
|
|
213
|
+
if (!target) throw new RepositoryError(`gitfile '${filePath}' names no directory`);
|
|
214
|
+
return path.resolve(path.dirname(filePath), target);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Follow a linked worktree's `commondir` file.
|
|
219
|
+
* @param {String} gitdir
|
|
220
|
+
* @returns {Promise<String>}
|
|
221
|
+
*/
|
|
222
|
+
async function resolveCommonDir(gitdir) {
|
|
223
|
+
const raw = await readFileOrNull(path.join(gitdir, 'commondir'));
|
|
224
|
+
if (!raw) return gitdir;
|
|
225
|
+
const target = raw.toString('utf-8').trim();
|
|
226
|
+
return path.resolve(gitdir, target);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Locate the git directory by walking up from a starting directory.
|
|
231
|
+
* @param {String} [startDir]
|
|
232
|
+
* @returns {Promise<{gitdir: String, commondir: String, worktree: String|null}>}
|
|
233
|
+
*/
|
|
234
|
+
async function findGitdir(startDir = process.cwd()) {
|
|
235
|
+
if (process.env.GIT_DIR) {
|
|
236
|
+
const gitdir = path.resolve(process.env.GIT_DIR);
|
|
237
|
+
return {
|
|
238
|
+
gitdir,
|
|
239
|
+
commondir: await resolveCommonDir(gitdir),
|
|
240
|
+
worktree: process.env.GIT_WORK_TREE ? path.resolve(process.env.GIT_WORK_TREE) : null
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let current = await fs.realpath(startDir);
|
|
245
|
+
const root = path.parse(current).root;
|
|
246
|
+
|
|
247
|
+
for (;;) {
|
|
248
|
+
for (const name of [GENT_DIR, GIT_DIR]) {
|
|
249
|
+
const candidate = path.join(current, name);
|
|
250
|
+
let stat;
|
|
251
|
+
try {
|
|
252
|
+
stat = await fs.stat(candidate);
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (stat.isDirectory()) {
|
|
258
|
+
if (name === GENT_DIR && !(await looksLikeGitdir(candidate)) && !(await looksLikeLegacyGentdir(candidate))) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const commondir = await resolveCommonDir(candidate);
|
|
262
|
+
return { gitdir: candidate, commondir, worktree: current };
|
|
263
|
+
}
|
|
264
|
+
if (stat.isFile()) {
|
|
265
|
+
const target = await readGitfile(candidate);
|
|
266
|
+
if (!target) continue;
|
|
267
|
+
const commondir = await resolveCommonDir(target);
|
|
268
|
+
return { gitdir: target, commondir, worktree: current };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A bare repository: the directory we are standing in *is* the gitdir.
|
|
273
|
+
if (await looksLikeGitdir(current)) {
|
|
274
|
+
return { gitdir: current, commondir: await resolveCommonDir(current), worktree: null };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (current === root) {
|
|
278
|
+
throw new RepositoryError(
|
|
279
|
+
`not a Gent repository (or any parent up to ${root}): no .gent or .git found`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
current = path.dirname(current);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {String} dir
|
|
288
|
+
* @returns {Promise<Boolean>}
|
|
289
|
+
*/
|
|
290
|
+
async function looksLikeGitdir(dir) {
|
|
291
|
+
try {
|
|
292
|
+
const [head, objects, refs] = await Promise.all([
|
|
293
|
+
fs.stat(path.join(dir, 'HEAD')).then(() => true, () => false),
|
|
294
|
+
fs.stat(path.join(dir, 'objects')).then(s => s.isDirectory(), () => false),
|
|
295
|
+
fs.stat(path.join(dir, 'refs')).then(s => s.isDirectory(), () => false)
|
|
296
|
+
]);
|
|
297
|
+
return head && objects && refs;
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {String} dir
|
|
305
|
+
* @returns {Promise<Boolean>}
|
|
306
|
+
*/
|
|
307
|
+
async function looksLikeLegacyGentdir(dir) {
|
|
308
|
+
return fs.access(path.join(dir, 'commits.json')).then(() => true, () => false);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Build the layered config: system < global < local < worktree.
|
|
313
|
+
* @param {String} commondir
|
|
314
|
+
* @param {String} gitdir
|
|
315
|
+
* @returns {Promise<{set: ConfigSet, local: ConfigFile}>}
|
|
316
|
+
*/
|
|
317
|
+
async function loadConfig(commondir, gitdir) {
|
|
318
|
+
const localPath = path.join(commondir, 'config');
|
|
319
|
+
const local = (await ConfigFile.load(localPath)) || new ConfigFile('', localPath);
|
|
320
|
+
|
|
321
|
+
const context = { gitdir: commondir, worktree: null, branch: null };
|
|
322
|
+
const files = [];
|
|
323
|
+
|
|
324
|
+
if (!process.env.GIT_CONFIG_NOSYSTEM) {
|
|
325
|
+
for (const candidate of ['/etc/gitconfig', '/usr/local/etc/gitconfig']) {
|
|
326
|
+
files.push(...await expandIncludes(await ConfigFile.load(candidate), context));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
331
|
+
for (const candidate of [path.join(xdg, 'git', 'config'), path.join(os.homedir(), '.gitconfig')]) {
|
|
332
|
+
files.push(...await expandIncludes(await ConfigFile.load(candidate), context));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
files.push(...await expandIncludes(local, context));
|
|
336
|
+
|
|
337
|
+
const set = new ConfigSet(files);
|
|
338
|
+
if (set.getBoolean('extensions.worktreeConfig', false)) {
|
|
339
|
+
const worktreeConfig = await ConfigFile.load(path.join(gitdir, 'config.worktree'));
|
|
340
|
+
if (worktreeConfig) files.push(...await expandIncludes(worktreeConfig, context));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { set: new ConfigSet(files), local };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Refuse repositories whose format Gent does not implement.
|
|
348
|
+
* @param {ConfigSet} config
|
|
349
|
+
* @param {String} commondir
|
|
350
|
+
*/
|
|
351
|
+
function assertSupportedFormat(config, commondir) {
|
|
352
|
+
const version = config.getInt('core.repositoryformatversion', 0);
|
|
353
|
+
const violations = [];
|
|
354
|
+
|
|
355
|
+
if (version > REPOSITORY_FORMAT_VERSION) {
|
|
356
|
+
throw new RepositoryError(
|
|
357
|
+
`repository format version ${version} at ${commondir} is newer than this Gent understands (${REPOSITORY_FORMAT_VERSION})`,
|
|
358
|
+
'GENT_UNKNOWN_FORMAT'
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const objectFormat = version >= 1 ? config.get('extensions.objectFormat') : undefined;
|
|
363
|
+
if (objectFormat === undefined) {
|
|
364
|
+
violations.push({
|
|
365
|
+
...feature('object-format.sha1'),
|
|
366
|
+
detail: `${commondir} has no extensions.objectFormat, so it is a SHA-1 repository`
|
|
367
|
+
});
|
|
368
|
+
} else if (objectFormat.toLowerCase() !== OBJECT_FORMAT) {
|
|
369
|
+
violations.push({ ...feature('object-format.sha1'), detail: `extensions.objectFormat is '${objectFormat}'` });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (version >= 1) {
|
|
373
|
+
for (const [name] of config.list()) {
|
|
374
|
+
if (!name.startsWith('extensions.')) continue;
|
|
375
|
+
const extension = name.slice('extensions.'.length).toLowerCase();
|
|
376
|
+
if (extension === 'refstorage') {
|
|
377
|
+
const backend = config.get(name);
|
|
378
|
+
if (backend && backend.toLowerCase() !== 'files') {
|
|
379
|
+
violations.push({ ...feature('refs.reftable'), detail: `extensions.refStorage is '${backend}'` });
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (extension === 'partialclone') {
|
|
384
|
+
violations.push({ ...feature('transport.shallow'), detail: `extensions.partialClone is set (promisor remote '${config.get(name)}')` });
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!KNOWN_EXTENSIONS.has(extension)) {
|
|
388
|
+
violations.push({
|
|
389
|
+
id: `extensions.${extension}`,
|
|
390
|
+
status: 'unsupported',
|
|
391
|
+
title: `Repository extension '${extension}'`,
|
|
392
|
+
detail: `${commondir} requires an extension Gent does not implement`,
|
|
393
|
+
remedy: 'Use Git for this repository, or remove the extension if it is no longer needed.'
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (violations.length) throw new UnsupportedFeatureError(violations, `opening ${commondir}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Detect a v12 repository so the caller can explain migration instead of
|
|
404
|
+
* failing with a confusing format error.
|
|
405
|
+
* @param {String} gitdir
|
|
406
|
+
* @returns {Promise<Boolean>}
|
|
407
|
+
*/
|
|
408
|
+
async function isLegacyRepository(gitdir) {
|
|
409
|
+
const hasCommitsJson = await fs.access(path.join(gitdir, 'commits.json')).then(() => true, () => false);
|
|
410
|
+
if (!hasCommitsJson) return false;
|
|
411
|
+
const hasCanonicalConfig = await fs.access(path.join(gitdir, 'config')).then(() => true, () => false);
|
|
412
|
+
return !hasCanonicalConfig;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Open the repository containing `startDir`.
|
|
417
|
+
* @param {String} [startDir]
|
|
418
|
+
* @returns {Promise<Repository>}
|
|
419
|
+
*/
|
|
420
|
+
async function open(startDir = process.cwd()) {
|
|
421
|
+
const located = await findGitdir(startDir);
|
|
422
|
+
|
|
423
|
+
if (await isLegacyRepository(located.commondir)) {
|
|
424
|
+
throw new LegacyRepositoryError(located.commondir);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const { set, local } = await loadConfig(located.commondir, located.gitdir);
|
|
428
|
+
assertSupportedFormat(set, located.commondir);
|
|
429
|
+
|
|
430
|
+
const bare = set.getBoolean('core.bare', located.worktree === null);
|
|
431
|
+
let worktree = bare ? null : located.worktree;
|
|
432
|
+
|
|
433
|
+
const configuredWorktree = set.get('core.worktree');
|
|
434
|
+
if (!bare && configuredWorktree) {
|
|
435
|
+
worktree = path.resolve(located.commondir, configuredWorktree);
|
|
436
|
+
}
|
|
437
|
+
if (!bare && !worktree) {
|
|
438
|
+
const raw = await readFileOrNull(path.join(located.gitdir, 'gitdir'));
|
|
439
|
+
if (raw) worktree = path.dirname(raw.toString('utf-8').trim());
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return new Repository({
|
|
443
|
+
gitdir: located.gitdir,
|
|
444
|
+
commondir: located.commondir,
|
|
445
|
+
worktree,
|
|
446
|
+
bare,
|
|
447
|
+
config: set,
|
|
448
|
+
localConfig: local
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Create a new repository.
|
|
454
|
+
*
|
|
455
|
+
* Gent-created repositories keep the `.gent` directory name and add a `.git`
|
|
456
|
+
* gitfile so tools with hard-coded discovery still find them. `.gent` itself
|
|
457
|
+
* is excluded through info/exclude, not through a hidden built-in rule.
|
|
458
|
+
*
|
|
459
|
+
* @param {String} directory
|
|
460
|
+
* @param {Object} [options]
|
|
461
|
+
* @param {Boolean} [options.bare]
|
|
462
|
+
* @param {String} [options.defaultBranch]
|
|
463
|
+
* @param {Boolean} [options.useGitDirName] - name the directory .git instead
|
|
464
|
+
* @returns {Promise<{repo: Repository, created: Boolean, gitdir: String}>}
|
|
465
|
+
*/
|
|
466
|
+
async function init(directory, options = {}) {
|
|
467
|
+
await fs.mkdir(path.resolve(directory), { recursive: true });
|
|
468
|
+
const root = await fs.realpath(directory);
|
|
469
|
+
// Reinitialization must never turn legacy/SHA-1 metadata into SHA-256
|
|
470
|
+
// metadata or overwrite the pointer of another repository.
|
|
471
|
+
for (const name of (options.bare ? ['HEAD'] : [GIT_DIR, GENT_DIR])) {
|
|
472
|
+
if (await fs.lstat(path.join(root, name)).then(() => true, error => {
|
|
473
|
+
if (error.code === 'ENOENT') return false;
|
|
474
|
+
throw error;
|
|
475
|
+
})) {
|
|
476
|
+
const repo = await open(root);
|
|
477
|
+
return { repo, created: false, gitdir: repo.gitdir };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const dirName = options.bare ? '' : (options.useGitDirName ? GIT_DIR : GENT_DIR);
|
|
481
|
+
const gitdir = options.bare ? root : path.join(root, dirName);
|
|
482
|
+
|
|
483
|
+
const existed = await fs.access(path.join(gitdir, 'HEAD')).then(() => true, () => false);
|
|
484
|
+
|
|
485
|
+
await fs.mkdir(path.join(gitdir, 'objects', 'pack'), { recursive: true });
|
|
486
|
+
await fs.mkdir(path.join(gitdir, 'objects', 'info'), { recursive: true });
|
|
487
|
+
await fs.mkdir(path.join(gitdir, 'refs', 'heads'), { recursive: true });
|
|
488
|
+
await fs.mkdir(path.join(gitdir, 'refs', 'tags'), { recursive: true });
|
|
489
|
+
await fs.mkdir(path.join(gitdir, 'info'), { recursive: true });
|
|
490
|
+
await fs.mkdir(path.join(gitdir, 'gent'), { recursive: true });
|
|
491
|
+
|
|
492
|
+
const branch = options.defaultBranch || 'main';
|
|
493
|
+
if (!existed) {
|
|
494
|
+
await writeAtomic(path.join(gitdir, 'HEAD'), `ref: refs/heads/${branch}\n`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const configPath = path.join(gitdir, 'config');
|
|
498
|
+
const config = (await ConfigFile.load(configPath)) || new ConfigFile('', configPath);
|
|
499
|
+
config.set('core.repositoryformatversion', String(REPOSITORY_FORMAT_VERSION));
|
|
500
|
+
config.set('core.filemode', 'true');
|
|
501
|
+
config.set('core.bare', options.bare ? 'true' : 'false');
|
|
502
|
+
if (!options.bare) config.set('core.logallrefupdates', 'true');
|
|
503
|
+
config.set('extensions.objectFormat', OBJECT_FORMAT);
|
|
504
|
+
config.set('gent.format', FORMAT_MARKER);
|
|
505
|
+
await config.save();
|
|
506
|
+
|
|
507
|
+
// Exclude our own metadata directory the way Git excludes anything else.
|
|
508
|
+
if (!options.bare && dirName === GENT_DIR) {
|
|
509
|
+
const excludePath = path.join(gitdir, 'info', 'exclude');
|
|
510
|
+
const existing = (await readFileOrNull(excludePath))?.toString('utf-8') || '';
|
|
511
|
+
if (!existing.split('\n').some(line => line.trim() === '/.gent/')) {
|
|
512
|
+
await writeAtomic(excludePath, `${existing}${existing && !existing.endsWith('\n') ? '\n' : ''}/.gent/\n`);
|
|
513
|
+
}
|
|
514
|
+
await writeAtomic(path.join(root, GIT_DIR), `gitdir: ${GENT_DIR}\n`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return { repo: await open(options.bare ? gitdir : root), created: !existed, gitdir };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
module.exports = {
|
|
521
|
+
Repository,
|
|
522
|
+
RepositoryError,
|
|
523
|
+
LegacyRepositoryError,
|
|
524
|
+
open,
|
|
525
|
+
init,
|
|
526
|
+
findGitdir,
|
|
527
|
+
isLegacyRepository,
|
|
528
|
+
readGitfile,
|
|
529
|
+
resolveCommonDir,
|
|
530
|
+
loadConfig,
|
|
531
|
+
GENT_DIR,
|
|
532
|
+
GIT_DIR
|
|
533
|
+
};
|