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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Attributes - gitattributes lookup and text/EOL conversion
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Decide how a path's bytes differ between the object store and the working
|
|
8
|
+
* tree, and refuse — before writing anything — when the answer depends on a
|
|
9
|
+
* transformation Gent does not implement.
|
|
10
|
+
*
|
|
11
|
+
* SUPPORTED:
|
|
12
|
+
* text, -text, text=auto, eol=lf, eol=crlf, the `binary` macro,
|
|
13
|
+
* core.autocrlf and core.eol.
|
|
14
|
+
*
|
|
15
|
+
* REFUSED (never guessed):
|
|
16
|
+
* filter=* (including Git LFS) and working-tree-encoding. A path carrying
|
|
17
|
+
* either raises UnsupportedFeatureError, so an operation stops before it
|
|
18
|
+
* corrupts content by storing the wrong bytes.
|
|
19
|
+
*
|
|
20
|
+
* DIRECTION:
|
|
21
|
+
* toIndex() working tree -> object store ("clean")
|
|
22
|
+
* toWorktree() object store -> working tree ("smudge")
|
|
23
|
+
* Blobs are stored with LF line endings; CRLF is a working-tree property.
|
|
24
|
+
* ============================================================================
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
const { readFileOrNull } = require('./lockfile');
|
|
30
|
+
const { IgnorePattern } = require('./ignore');
|
|
31
|
+
const { UnsupportedFeatureError, feature } = require('./feature-support');
|
|
32
|
+
|
|
33
|
+
/** Attributes that decide content transformation. */
|
|
34
|
+
const BINARY_MACRO = { text: false, diff: false, merge: false };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {Buffer} buffer
|
|
38
|
+
* @returns {Boolean} Git's heuristic: a NUL byte in the first 8000 bytes
|
|
39
|
+
*/
|
|
40
|
+
function looksBinary(buffer) {
|
|
41
|
+
const limit = Math.min(buffer.length, 8000);
|
|
42
|
+
return buffer.indexOf(0, 0) >= 0 && buffer.indexOf(0, 0) < limit;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One `<pattern> <attr>...` line.
|
|
47
|
+
*/
|
|
48
|
+
class AttributeRule {
|
|
49
|
+
/**
|
|
50
|
+
* @param {String} line
|
|
51
|
+
* @param {String} base - POSIX directory the file sits in
|
|
52
|
+
*/
|
|
53
|
+
constructor(line, base) {
|
|
54
|
+
const parts = line.trim().split(/\s+/);
|
|
55
|
+
const pattern = parts.shift();
|
|
56
|
+
this.matcher = new IgnorePattern(pattern.endsWith('/') ? pattern : pattern, base);
|
|
57
|
+
this.attributes = {};
|
|
58
|
+
|
|
59
|
+
for (const token of parts) {
|
|
60
|
+
if (token.startsWith('-')) { this.attributes[token.slice(1)] = false; continue; }
|
|
61
|
+
if (token.startsWith('!')) { this.attributes[token.slice(1)] = undefined; continue; }
|
|
62
|
+
const eq = token.indexOf('=');
|
|
63
|
+
if (eq < 0) { this.attributes[token] = true; continue; }
|
|
64
|
+
this.attributes[token.slice(0, eq)] = token.slice(eq + 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {String} relativePath
|
|
70
|
+
* @param {Boolean} isDirectory
|
|
71
|
+
* @returns {Boolean}
|
|
72
|
+
*/
|
|
73
|
+
matches(relativePath, isDirectory) {
|
|
74
|
+
return this.matcher.matches(relativePath, isDirectory);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {String} text
|
|
80
|
+
* @param {String} base
|
|
81
|
+
* @returns {Array<AttributeRule>}
|
|
82
|
+
*/
|
|
83
|
+
function parseAttributes(text, base = '') {
|
|
84
|
+
const rules = [];
|
|
85
|
+
for (const raw of text.split('\n')) {
|
|
86
|
+
const line = raw.replace(/\r$/, '').trim();
|
|
87
|
+
if (!line || line.startsWith('#')) continue;
|
|
88
|
+
rules.push(new AttributeRule(line, base));
|
|
89
|
+
}
|
|
90
|
+
return rules;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class AttributesMatcher {
|
|
94
|
+
/**
|
|
95
|
+
* @param {Object} repo
|
|
96
|
+
*/
|
|
97
|
+
constructor(repo) {
|
|
98
|
+
this.repo = repo;
|
|
99
|
+
this.baseRules = [];
|
|
100
|
+
this.perDirectory = new Map();
|
|
101
|
+
this.loaded = false;
|
|
102
|
+
|
|
103
|
+
this.autocrlf = repo.config.get('core.autocrlf', 'false').toLowerCase();
|
|
104
|
+
this.eol = repo.config.get('core.eol', 'native').toLowerCase();
|
|
105
|
+
this.safecrlf = repo.config.get('core.safecrlf', 'false').toLowerCase();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async load() {
|
|
109
|
+
if (this.loaded) return;
|
|
110
|
+
this.loaded = true;
|
|
111
|
+
|
|
112
|
+
const infoAttributes = await readFileOrNull(path.join(this.repo.commondir, 'info', 'attributes'));
|
|
113
|
+
if (infoAttributes) this.baseRules.push(...parseAttributes(infoAttributes.toString('utf-8')));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {String} directory
|
|
118
|
+
* @returns {Promise<Array<AttributeRule>>}
|
|
119
|
+
*/
|
|
120
|
+
async rulesFor(directory) {
|
|
121
|
+
if (this.perDirectory.has(directory)) return this.perDirectory.get(directory);
|
|
122
|
+
|
|
123
|
+
const filePath = path.join(this.repo.worktree, ...(directory ? directory.split('/') : []), '.gitattributes');
|
|
124
|
+
const text = await readFileOrNull(filePath);
|
|
125
|
+
const rules = text ? parseAttributes(text.toString('utf-8'), directory) : [];
|
|
126
|
+
this.perDirectory.set(directory, rules);
|
|
127
|
+
return rules;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {String} relativePath
|
|
132
|
+
* @returns {Promise<Object>} resolved attribute values
|
|
133
|
+
*/
|
|
134
|
+
async attributesFor(relativePath) {
|
|
135
|
+
await this.load();
|
|
136
|
+
|
|
137
|
+
const resolved = {};
|
|
138
|
+
const apply = (rule) => {
|
|
139
|
+
if (!rule.matches(relativePath, false)) return;
|
|
140
|
+
const source = rule.attributes.binary === true ? { ...BINARY_MACRO, ...rule.attributes } : rule.attributes;
|
|
141
|
+
for (const [key, value] of Object.entries(source)) {
|
|
142
|
+
if (key === 'binary') continue;
|
|
143
|
+
resolved[key] = value;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const rule of this.baseRules) apply(rule);
|
|
148
|
+
|
|
149
|
+
const components = relativePath.split('/');
|
|
150
|
+
for (let depth = 0; depth < components.length; depth++) {
|
|
151
|
+
for (const rule of await this.rulesFor(components.slice(0, depth).join('/'))) apply(rule);
|
|
152
|
+
}
|
|
153
|
+
return resolved;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Refuse paths whose bytes depend on an unimplemented transformation.
|
|
158
|
+
* @param {String} relativePath
|
|
159
|
+
* @param {Object} attributes
|
|
160
|
+
* @param {String} what
|
|
161
|
+
*/
|
|
162
|
+
assertConvertible(relativePath, attributes, what) {
|
|
163
|
+
const problems = [];
|
|
164
|
+
if (attributes.filter !== undefined && attributes.filter !== false) {
|
|
165
|
+
problems.push({
|
|
166
|
+
...feature('attributes.filter'),
|
|
167
|
+
detail: `'${relativePath}' has filter=${attributes.filter}; its stored bytes are produced by an external program.`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (attributes['working-tree-encoding'] !== undefined && attributes['working-tree-encoding'] !== false) {
|
|
171
|
+
problems.push({
|
|
172
|
+
id: 'attributes.encoding',
|
|
173
|
+
status: 'unsupported',
|
|
174
|
+
title: 'working-tree-encoding attribute',
|
|
175
|
+
detail: `'${relativePath}' declares working-tree-encoding=${attributes['working-tree-encoding']}.`,
|
|
176
|
+
remedy: 'Check this path out with Git, or remove the attribute.'
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (problems.length) throw new UnsupportedFeatureError(problems, what);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Decide whether CRLF conversion applies to a path.
|
|
184
|
+
* @param {String} relativePath
|
|
185
|
+
* @param {Buffer} sample - content used for the text=auto heuristic
|
|
186
|
+
* @returns {Promise<{convert: Boolean, worktreeEol: 'lf'|'crlf'}>}
|
|
187
|
+
*/
|
|
188
|
+
async conversionFor(relativePath, sample) {
|
|
189
|
+
const attributes = await this.attributesFor(relativePath);
|
|
190
|
+
this.assertConvertible(relativePath, attributes, 'converting line endings');
|
|
191
|
+
|
|
192
|
+
let isText;
|
|
193
|
+
if (attributes.text === false) isText = false;
|
|
194
|
+
else if (attributes.text === true) isText = true;
|
|
195
|
+
else if (attributes.text === 'auto') isText = !looksBinary(sample);
|
|
196
|
+
else if (this.autocrlf === 'true' || this.autocrlf === 'input') isText = !looksBinary(sample);
|
|
197
|
+
else isText = false;
|
|
198
|
+
|
|
199
|
+
if (!isText) return { convert: false, worktreeEol: 'lf' };
|
|
200
|
+
|
|
201
|
+
let worktreeEol;
|
|
202
|
+
if (attributes.eol === 'crlf') worktreeEol = 'crlf';
|
|
203
|
+
else if (attributes.eol === 'lf') worktreeEol = 'lf';
|
|
204
|
+
else if (this.autocrlf === 'true') worktreeEol = 'crlf';
|
|
205
|
+
else if (this.autocrlf === 'input') worktreeEol = 'lf';
|
|
206
|
+
else if (this.eol === 'crlf') worktreeEol = 'crlf';
|
|
207
|
+
else if (this.eol === 'native') worktreeEol = process.platform === 'win32' ? 'crlf' : 'lf';
|
|
208
|
+
else worktreeEol = 'lf';
|
|
209
|
+
|
|
210
|
+
return { convert: true, worktreeEol };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Working tree bytes -> stored blob bytes.
|
|
215
|
+
* @param {String} relativePath
|
|
216
|
+
* @param {Buffer} content
|
|
217
|
+
* @returns {Promise<Buffer>}
|
|
218
|
+
*/
|
|
219
|
+
async toIndex(relativePath, content) {
|
|
220
|
+
const { convert } = await this.conversionFor(relativePath, content);
|
|
221
|
+
if (!convert) return content;
|
|
222
|
+
return normalizeToLf(content);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Stored blob bytes -> working tree bytes.
|
|
227
|
+
* @param {String} relativePath
|
|
228
|
+
* @param {Buffer} content
|
|
229
|
+
* @returns {Promise<Buffer>}
|
|
230
|
+
*/
|
|
231
|
+
async toWorktree(relativePath, content) {
|
|
232
|
+
const { convert, worktreeEol } = await this.conversionFor(relativePath, content);
|
|
233
|
+
if (!convert || worktreeEol !== 'crlf') return content;
|
|
234
|
+
return normalizeToCrlf(content);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {Buffer} content
|
|
240
|
+
* @returns {Buffer}
|
|
241
|
+
*/
|
|
242
|
+
function normalizeToLf(content) {
|
|
243
|
+
if (!content.includes(0x0d)) return content;
|
|
244
|
+
|
|
245
|
+
const out = Buffer.allocUnsafe(content.length);
|
|
246
|
+
let written = 0;
|
|
247
|
+
for (let i = 0; i < content.length; i++) {
|
|
248
|
+
if (content[i] === 0x0d && content[i + 1] === 0x0a) continue; // drop the CR of a CRLF
|
|
249
|
+
out[written++] = content[i];
|
|
250
|
+
}
|
|
251
|
+
return out.subarray(0, written);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {Buffer} content
|
|
256
|
+
* @returns {Buffer}
|
|
257
|
+
*/
|
|
258
|
+
function normalizeToCrlf(content) {
|
|
259
|
+
const lf = normalizeToLf(content);
|
|
260
|
+
let count = 0;
|
|
261
|
+
for (let i = 0; i < lf.length; i++) if (lf[i] === 0x0a) count++;
|
|
262
|
+
if (count === 0) return lf;
|
|
263
|
+
|
|
264
|
+
const out = Buffer.allocUnsafe(lf.length + count);
|
|
265
|
+
let written = 0;
|
|
266
|
+
for (let i = 0; i < lf.length; i++) {
|
|
267
|
+
if (lf[i] === 0x0a) out[written++] = 0x0d;
|
|
268
|
+
out[written++] = lf[i];
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
AttributesMatcher,
|
|
275
|
+
AttributeRule,
|
|
276
|
+
parseAttributes,
|
|
277
|
+
looksBinary,
|
|
278
|
+
normalizeToLf,
|
|
279
|
+
normalizeToCrlf
|
|
280
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Exact local checkpoints for canonical undo/redo. Private refs retain every
|
|
2
|
+
* object needed by a checkpoint when external Git runs garbage collection.
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { GitIndex, IndexEntry } = require('./git-index');
|
|
8
|
+
const { IgnoreMatcher, walkWorktree } = require('./ignore');
|
|
9
|
+
const { serializeCommit, serializeTree, MODE } = require('./git-objects');
|
|
10
|
+
const { writeAtomic, readFileOrNull } = require('./lockfile');
|
|
11
|
+
const worktree = require('./worktree');
|
|
12
|
+
|
|
13
|
+
const journalPath = repo => path.join(repo.gentWorktreeMetaDir, 'journal.json');
|
|
14
|
+
async function read(repo) {
|
|
15
|
+
const bytes = await readFileOrNull(journalPath(repo));
|
|
16
|
+
return bytes ? JSON.parse(bytes.toString()) : { undo: [], redo: [] };
|
|
17
|
+
}
|
|
18
|
+
async function save(repo, journal) { await writeAtomic(journalPath(repo), JSON.stringify(journal)); }
|
|
19
|
+
|
|
20
|
+
async function capture(repo) {
|
|
21
|
+
const index = await GitIndex.read(repo.indexPath);
|
|
22
|
+
const files = {};
|
|
23
|
+
const matcher = new IgnoreMatcher(repo);
|
|
24
|
+
const names = new Set(index.entries.map(e => e.path));
|
|
25
|
+
for await (const entry of walkWorktree(repo, matcher, { tracked: names })) names.add(entry.path);
|
|
26
|
+
for (const name of [...names].sort()) {
|
|
27
|
+
const file = await worktree.snapshotPath(repo, name);
|
|
28
|
+
if (file) files[name] = { mode: file.mode, oid: await repo.objects.write('blob', Buffer.from(file.content, 'base64')) };
|
|
29
|
+
}
|
|
30
|
+
const refs = Object.fromEntries([...(await repo.refs.list())].filter(([name]) => !name.startsWith('refs/gent/')).sort(([a], [b]) => a.localeCompare(b)));
|
|
31
|
+
const merge = {};
|
|
32
|
+
for (const name of ['MERGE_HEAD', 'MERGE_MSG', 'MERGE_MODE', 'ORIG_HEAD']) merge[name] = (await readFileOrNull(repo.gitPath(name)))?.toString('base64') ?? null;
|
|
33
|
+
return { head: await repo.refs.head(), refs, index: index.sourceBytes?.toString('base64') ?? null, files, merge };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function retain(repo, state, prefix) {
|
|
37
|
+
const entries = [];
|
|
38
|
+
// These are retention objects, not worktree trees. Numeric names allow
|
|
39
|
+
// conflicted index stages and staged/unstaged versions to coexist.
|
|
40
|
+
for (const item of Object.values(state.files)) entries.push({ name: String(entries.length), mode: MODE.REGULAR, oid: item.oid });
|
|
41
|
+
if (state.index) {
|
|
42
|
+
for (const item of GitIndex.parse(Buffer.from(state.index, 'base64')).entries) {
|
|
43
|
+
if (item.mode === MODE.GITLINK) throw new Error('journal checkpoints do not support submodules');
|
|
44
|
+
entries.push({ name: String(entries.length), mode: MODE.REGULAR, oid: item.oid });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const tree = await repo.objects.write('tree', serializeTree(entries));
|
|
48
|
+
const identity = { name: 'Gent checkpoint', email: 'checkpoint@gent.local', timestamp: Math.floor(Date.now() / 1000), timezone: '+0000' };
|
|
49
|
+
const oid = await repo.objects.write('commit', serializeCommit({ tree, parents: state.head.oid ? [state.head.oid] : [], author: identity, committer: identity, message: Buffer.from('Gent undo checkpoint\n') }));
|
|
50
|
+
await repo.refs.update(`${prefix}/snapshot`, oid, { expectedOldOid: null, reason: 'retain undo checkpoint' });
|
|
51
|
+
let n = 0;
|
|
52
|
+
for (const target of new Set(Object.values(state.refs))) await repo.refs.update(`${prefix}/ref-${n++}`, target, { expectedOldOid: null, reason: 'retain undo ref' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function begin(repo, name) {
|
|
56
|
+
const before = await capture(repo);
|
|
57
|
+
const id = crypto.randomUUID();
|
|
58
|
+
await retain(repo, before, `refs/gent/journal/${id}/before`);
|
|
59
|
+
return { id, name, before };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function finish(repo, entry) {
|
|
63
|
+
entry.after = await capture(repo);
|
|
64
|
+
await retain(repo, entry.after, `refs/gent/journal/${entry.id}/after`);
|
|
65
|
+
const journal = await read(repo);
|
|
66
|
+
journal.undo.push(entry);
|
|
67
|
+
journal.redo = [];
|
|
68
|
+
await save(repo, journal);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sameState(a, b) {
|
|
72
|
+
// Index stat refreshes by other tools are harmless. Compare staged content
|
|
73
|
+
// and flags, not filesystem cache timestamps.
|
|
74
|
+
const semantic = state => {
|
|
75
|
+
const entries = state.index ? GitIndex.parse(Buffer.from(state.index, 'base64')).entries : [];
|
|
76
|
+
return { ...state, index: entries.map(e => ({ path: e.path, oid: e.oid, mode: e.mode, stage: e.stage, assumeValid: e.assumeValid, skipWorktree: e.skipWorktree, intentToAdd: e.intentToAdd })) };
|
|
77
|
+
};
|
|
78
|
+
return JSON.stringify(semantic(a)) === JSON.stringify(semantic(b));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function restore(repo, redo = false) {
|
|
82
|
+
await worktree.assertNoPendingCheckout(repo, 'undo/redo');
|
|
83
|
+
const journal = await read(repo);
|
|
84
|
+
const source = redo ? journal.redo : journal.undo;
|
|
85
|
+
const entry = source[source.length - 1];
|
|
86
|
+
if (!entry) throw new Error(`nothing to ${redo ? 'redo' : 'undo'}`);
|
|
87
|
+
const expected = redo ? entry.before : entry.after;
|
|
88
|
+
const target = redo ? entry.after : entry.before;
|
|
89
|
+
const current = await capture(repo);
|
|
90
|
+
if (!sameState(current, expected)) throw new Error('repository changed since the recorded operation; refusing to overwrite intervening work');
|
|
91
|
+
const index = await GitIndex.read(repo.indexPath);
|
|
92
|
+
const targetIndex = target.index ? GitIndex.parse(Buffer.from(target.index, 'base64')) : new GitIndex();
|
|
93
|
+
const from = new Map(Object.entries(current.files));
|
|
94
|
+
const to = new Map(Object.entries(target.files));
|
|
95
|
+
const plan = await worktree.planCheckout(repo, { from, to, index, force: true });
|
|
96
|
+
// Snapshots store exact working bytes, so EOL conversion must not run twice.
|
|
97
|
+
for (const write of plan.writes) write.content = (await repo.objects.readBlob(write.oid)).toString('base64');
|
|
98
|
+
await worktree.applyCheckout(repo, plan, { index });
|
|
99
|
+
index.entries = targetIndex.entries;
|
|
100
|
+
index.extensions.clear();
|
|
101
|
+
await index.write(repo.indexPath);
|
|
102
|
+
for (const name of new Set([...Object.keys(current.refs), ...Object.keys(target.refs)])) {
|
|
103
|
+
if (current.refs[name] === target.refs[name]) continue;
|
|
104
|
+
if (target.refs[name]) await repo.refs.update(name, target.refs[name], { expectedOldOid: current.refs[name] || null, reason: redo ? 'redo' : 'undo' });
|
|
105
|
+
else await repo.refs.delete(name, { expectedOldOid: current.refs[name], reason: redo ? 'redo' : 'undo' });
|
|
106
|
+
}
|
|
107
|
+
// A branch ref may already have moved above; HEAD's expected oid follows it.
|
|
108
|
+
const expectedHead = { ...current.head, oid: current.head.ref ? (target.refs[current.head.ref] || null) : current.head.oid };
|
|
109
|
+
if (target.head.ref) await repo.refs.setHeadSymbolic(target.head.ref, redo ? 'redo' : 'undo', expectedHead);
|
|
110
|
+
else await repo.refs.setHeadDetached(target.head.oid, redo ? 'redo' : 'undo', expectedHead);
|
|
111
|
+
for (const [name, bytes] of Object.entries(target.merge)) {
|
|
112
|
+
if (bytes === null) await fs.rm(repo.gitPath(name), { force: true });
|
|
113
|
+
else await writeAtomic(repo.gitPath(name), Buffer.from(bytes, 'base64'));
|
|
114
|
+
}
|
|
115
|
+
source.pop();
|
|
116
|
+
(redo ? journal.undo : journal.redo).push(entry);
|
|
117
|
+
await save(repo, journal);
|
|
118
|
+
await worktree.completeCheckout(repo);
|
|
119
|
+
return entry.name;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { begin, finish, restore, read };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Feature Support Manifest
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* One declaration of what Gent v13 supports, consumed by repository opening,
|
|
8
|
+
* command preflight, the generated documentation and the test suites. The
|
|
9
|
+
* data lives in feature-support.json so the Django API can load the exact
|
|
10
|
+
* same bytes when server integration is implemented.
|
|
11
|
+
*
|
|
12
|
+
* RULE:
|
|
13
|
+
* Nothing may claim support for a feature this manifest does not mark
|
|
14
|
+
* 'supported'. Detection happens *before* any state is modified, so an
|
|
15
|
+
* unsupported repository is refused rather than half-converted.
|
|
16
|
+
*
|
|
17
|
+
* ============================================================================
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const manifest = require('./feature-support.json');
|
|
21
|
+
|
|
22
|
+
const BY_ID = new Map(manifest.features.map(f => [f.id, f]));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Raised when a repository or request needs something Gent does not implement.
|
|
26
|
+
* Carries the manifest entries so callers can render a consistent message.
|
|
27
|
+
*/
|
|
28
|
+
class UnsupportedFeatureError extends Error {
|
|
29
|
+
/**
|
|
30
|
+
* @param {Array<{id: String, title: String, detail: String, remedy?: String}>} features
|
|
31
|
+
* @param {String} [context] - what was being attempted
|
|
32
|
+
*/
|
|
33
|
+
constructor(features, context) {
|
|
34
|
+
const list = features.map(f => ` - ${f.title}: ${f.detail}${f.remedy ? `\n Fix: ${f.remedy}` : ''}`).join('\n');
|
|
35
|
+
super(`${context ? `${context}: ` : ''}unsupported repository feature\n${list}`);
|
|
36
|
+
this.name = 'UnsupportedFeatureError';
|
|
37
|
+
this.code = 'GENT_UNSUPPORTED';
|
|
38
|
+
this.features = features;
|
|
39
|
+
this.context = context || null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Look up a manifest entry.
|
|
45
|
+
* @param {String} id
|
|
46
|
+
* @returns {Object}
|
|
47
|
+
*/
|
|
48
|
+
function feature(id) {
|
|
49
|
+
const f = BY_ID.get(id);
|
|
50
|
+
if (!f) throw new Error(`Unknown feature id '${id}' — add it to feature-support.json`);
|
|
51
|
+
return f;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {String} id
|
|
56
|
+
* @returns {Boolean}
|
|
57
|
+
*/
|
|
58
|
+
function isSupported(id) {
|
|
59
|
+
const status = feature(id).status;
|
|
60
|
+
return status === 'supported' || status === 'partial';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Throw if any of the given feature ids is not implemented.
|
|
65
|
+
* @param {Array<String>} ids
|
|
66
|
+
* @param {String} [context]
|
|
67
|
+
*/
|
|
68
|
+
function assertSupported(ids, context) {
|
|
69
|
+
const bad = ids.filter(id => feature(id).status === 'unsupported').map(feature);
|
|
70
|
+
if (bad.length) throw new UnsupportedFeatureError(bad, context);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* All features in a given status, for documentation and status output.
|
|
75
|
+
* @param {String} status
|
|
76
|
+
* @returns {Array<Object>}
|
|
77
|
+
*/
|
|
78
|
+
function byStatus(status) {
|
|
79
|
+
return manifest.features.filter(f => f.status === status);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
manifest,
|
|
84
|
+
FORMAT_MARKER: manifest.formatMarker,
|
|
85
|
+
OBJECT_FORMAT: manifest.objectFormat,
|
|
86
|
+
REPOSITORY_FORMAT_VERSION: manifest.repositoryFormatVersion,
|
|
87
|
+
UnsupportedFeatureError,
|
|
88
|
+
feature,
|
|
89
|
+
isSupported,
|
|
90
|
+
assertSupported,
|
|
91
|
+
byStatus
|
|
92
|
+
};
|