pagan-artifact 0.3.2 → 0.3.3
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/bin/art.js +212 -60
- package/branching/index.js +266 -110
- package/caches/index.js +257 -77
- package/changes/index.js +93 -29
- package/contributions/index.js +249 -101
- package/index.js +2 -2
- package/package.json +1 -1
- package/setup/index.js +190 -55
- package/utils/constants.js +15 -0
- package/utils/getStateByHash/index.js +135 -78
- package/utils/shouldIgnore/index.js +44 -3
- package/workflow/index.js +256 -132
package/setup/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Artifact - Modern version control.
|
|
3
|
+
* @author Benny Schmidt (https://github.com/bennyschmidt)
|
|
4
|
+
* @project https://github.com/bennyschmidt/artifact
|
|
5
|
+
* Module: Setup (v0.3.3)
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
const fs = require('fs');
|
|
@@ -8,15 +10,20 @@ const path = require('path');
|
|
|
8
10
|
|
|
9
11
|
const pkg = require('../package.json');
|
|
10
12
|
const shouldIgnore = require('../utils/shouldIgnore');
|
|
13
|
+
const { MAX_PART_CHARS } = require('../utils/constants');
|
|
11
14
|
|
|
12
15
|
const ARTIFACT_HOST = pkg.artConfig.host || 'http://localhost:1337';
|
|
13
|
-
const MAX_PART_CHARS = 32000000;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Internal helper to create the .art directory tree.
|
|
19
|
+
* @param {string} artDirectory - The path to the .art folder.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
|
-
function ensureDirStructure(artDirectory) {
|
|
22
|
+
function ensureDirStructure (artDirectory) {
|
|
23
|
+
/**
|
|
24
|
+
* Define the core folder hierarchy required for local and remote tracking.
|
|
25
|
+
*/
|
|
26
|
+
|
|
20
27
|
const folders = [
|
|
21
28
|
'',
|
|
22
29
|
'root',
|
|
@@ -38,9 +45,15 @@ function ensureDirStructure(artDirectory) {
|
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* Initializes the local .art directory structure and indexes current files.
|
|
48
|
+
* @param {string} directoryPath - The root directory to initialize.
|
|
49
|
+
* @returns {string} Success message.
|
|
41
50
|
*/
|
|
42
51
|
|
|
43
52
|
function init (directoryPath = process.cwd()) {
|
|
53
|
+
/**
|
|
54
|
+
* Check for an existing repository to prevent accidental overwrites.
|
|
55
|
+
*/
|
|
56
|
+
|
|
44
57
|
const artDirectory = path.join(directoryPath, '.art');
|
|
45
58
|
|
|
46
59
|
if (fs.existsSync(artDirectory)) {
|
|
@@ -49,11 +62,15 @@ function init (directoryPath = process.cwd()) {
|
|
|
49
62
|
|
|
50
63
|
ensureDirStructure(artDirectory);
|
|
51
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Filter the working directory to find unignored files that are not internal.
|
|
67
|
+
*/
|
|
68
|
+
|
|
52
69
|
const files = fs.readdirSync(directoryPath, { recursive: true })
|
|
53
|
-
.filter(
|
|
54
|
-
const isInternal =
|
|
70
|
+
.filter(file => {
|
|
71
|
+
const isInternal = file === '.art' || file.startsWith('.art' + path.sep);
|
|
55
72
|
|
|
56
|
-
return !isInternal && !shouldIgnore(
|
|
73
|
+
return !isInternal && !shouldIgnore(file);
|
|
57
74
|
});
|
|
58
75
|
|
|
59
76
|
const rootMasterManifest = { parts: [] };
|
|
@@ -61,8 +78,14 @@ function init (directoryPath = process.cwd()) {
|
|
|
61
78
|
let currentPartFiles = [];
|
|
62
79
|
let currentPartChars = 0;
|
|
63
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Internal helper to save indexed file snapshots into manifest parts.
|
|
83
|
+
*/
|
|
84
|
+
|
|
64
85
|
const saveManifestPart = () => {
|
|
65
|
-
if (currentPartFiles.length === 0)
|
|
86
|
+
if (currentPartFiles.length === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
66
89
|
|
|
67
90
|
const partIndex = rootMasterManifest.parts.length;
|
|
68
91
|
const partName = `manifest.part.${partIndex}.json`;
|
|
@@ -78,6 +101,10 @@ function init (directoryPath = process.cwd()) {
|
|
|
78
101
|
currentPartChars = 0;
|
|
79
102
|
};
|
|
80
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Iterate through files and group their content into the root manifest.
|
|
106
|
+
*/
|
|
107
|
+
|
|
81
108
|
for (const file of files) {
|
|
82
109
|
const fullPath = path.join(directoryPath, file);
|
|
83
110
|
|
|
@@ -99,6 +126,10 @@ function init (directoryPath = process.cwd()) {
|
|
|
99
126
|
|
|
100
127
|
saveManifestPart();
|
|
101
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Initialize manifest files for root and default branch histories.
|
|
131
|
+
*/
|
|
132
|
+
|
|
102
133
|
fs.writeFileSync(
|
|
103
134
|
path.join(artDirectory, 'root/manifest.json'),
|
|
104
135
|
JSON.stringify(rootMasterManifest, null, 2)
|
|
@@ -114,6 +145,10 @@ function init (directoryPath = process.cwd()) {
|
|
|
114
145
|
JSON.stringify({ commits: [] }, null, 2)
|
|
115
146
|
);
|
|
116
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Create the primary art.json state file with default pointers.
|
|
150
|
+
*/
|
|
151
|
+
|
|
117
152
|
const artFile = {
|
|
118
153
|
active: { branch: 'main', parent: null },
|
|
119
154
|
remote: '',
|
|
@@ -130,11 +165,18 @@ function init (directoryPath = process.cwd()) {
|
|
|
130
165
|
|
|
131
166
|
/**
|
|
132
167
|
* Clone a repository and replay history.
|
|
168
|
+
* @param {string} repoSlug - The handle/repo identifier.
|
|
169
|
+
* @param {string} providedToken - Optional access token.
|
|
170
|
+
* @returns {Promise<string>} Success message.
|
|
133
171
|
*/
|
|
134
172
|
|
|
135
173
|
async function clone (repoSlug, providedToken = null) {
|
|
174
|
+
/**
|
|
175
|
+
* Validate slug format and check destination availability.
|
|
176
|
+
*/
|
|
177
|
+
|
|
136
178
|
if (!repoSlug || !repoSlug.includes('/')) {
|
|
137
|
-
throw new Error('A valid slug is required (
|
|
179
|
+
throw new Error('A valid slug is required (handle/repo).');
|
|
138
180
|
}
|
|
139
181
|
|
|
140
182
|
const [handle, repo] = repoSlug.split('/');
|
|
@@ -145,6 +187,10 @@ async function clone (repoSlug, providedToken = null) {
|
|
|
145
187
|
throw new Error(`Destination path "${targetPath}" already exists.`);
|
|
146
188
|
}
|
|
147
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Prepare target directory and enter the workspace.
|
|
192
|
+
*/
|
|
193
|
+
|
|
148
194
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
149
195
|
|
|
150
196
|
const artPath = path.join(targetPath, '.art');
|
|
@@ -153,44 +199,77 @@ async function clone (repoSlug, providedToken = null) {
|
|
|
153
199
|
process.chdir(targetPath);
|
|
154
200
|
|
|
155
201
|
try {
|
|
202
|
+
/**
|
|
203
|
+
* Set up local art.json with remote tracking info.
|
|
204
|
+
*/
|
|
205
|
+
|
|
156
206
|
const artJsonPath = path.join(artPath, 'art.json');
|
|
157
207
|
|
|
158
208
|
const artJson = {
|
|
159
209
|
active: { branch: 'main', parent: null },
|
|
160
210
|
remote: `${ARTIFACT_HOST}/${handle}/${repo}`,
|
|
161
|
-
configuration: {
|
|
211
|
+
configuration: {
|
|
212
|
+
handle: '',
|
|
213
|
+
personalAccessToken: providedToken || ''
|
|
214
|
+
}
|
|
162
215
|
};
|
|
163
216
|
|
|
164
217
|
fs.writeFileSync(artJsonPath, JSON.stringify(artJson, null, 2));
|
|
165
218
|
|
|
166
219
|
const token = artJson.configuration.personalAccessToken;
|
|
167
220
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Fetch the root manifest to reconstruct the project's base state.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
const rootRes = await fetch(
|
|
226
|
+
`${ARTIFACT_HOST}/manifest`,
|
|
227
|
+
{
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
type: 'root',
|
|
232
|
+
handle,
|
|
233
|
+
repo,
|
|
234
|
+
branch: 'main',
|
|
235
|
+
|
|
236
|
+
...(token && { personalAccessToken: token })
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
);
|
|
179
240
|
|
|
180
241
|
const masterManifest = await rootRes.json();
|
|
181
242
|
|
|
182
|
-
fs.writeFileSync(
|
|
243
|
+
fs.writeFileSync(
|
|
244
|
+
path.join(artPath, 'root/manifest.json'),
|
|
245
|
+
JSON.stringify(masterManifest, null, 2)
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Download root parts and write files to the working directory.
|
|
250
|
+
*/
|
|
183
251
|
|
|
184
252
|
for (const partName of masterManifest.parts) {
|
|
185
|
-
const partRes = await fetch(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
253
|
+
const partRes = await fetch(
|
|
254
|
+
`${ARTIFACT_HOST}/part`,
|
|
255
|
+
{
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: { 'Content-Type': 'application/json' },
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
handle,
|
|
260
|
+
repo,
|
|
261
|
+
partName,
|
|
262
|
+
...(token && { personalAccessToken: token })
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
);
|
|
190
266
|
|
|
191
267
|
const partData = await partRes.json();
|
|
192
268
|
|
|
193
|
-
fs.writeFileSync(
|
|
269
|
+
fs.writeFileSync(
|
|
270
|
+
path.join(artPath, 'root', partName),
|
|
271
|
+
JSON.stringify(partData, null, 2)
|
|
272
|
+
);
|
|
194
273
|
|
|
195
274
|
for (const file of partData.files) {
|
|
196
275
|
const workingPath = path.join(targetPath, file.path);
|
|
@@ -200,44 +279,79 @@ async function clone (repoSlug, providedToken = null) {
|
|
|
200
279
|
}
|
|
201
280
|
}
|
|
202
281
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
body: JSON.stringify({ type: 'history', handle, repo, branch: 'main', ...(token && { personalAccessToken: token }) })
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const historyManifest = await historyRes.json();
|
|
210
|
-
const localHistoryDir = path.join(artPath, 'history/local/main');
|
|
211
|
-
const remoteHistoryDir = path.join(artPath, 'history/remote/main');
|
|
282
|
+
/**
|
|
283
|
+
* Retrieve the full history manifest for the main branch.
|
|
284
|
+
*/
|
|
212
285
|
|
|
213
|
-
|
|
214
|
-
|
|
286
|
+
const historyRes = await fetch(
|
|
287
|
+
`${ARTIFACT_HOST}/manifest`,
|
|
288
|
+
{
|
|
215
289
|
method: 'POST',
|
|
216
290
|
headers: { 'Content-Type': 'application/json' },
|
|
217
291
|
body: JSON.stringify({
|
|
292
|
+
type: 'history',
|
|
218
293
|
handle,
|
|
219
294
|
repo,
|
|
220
295
|
branch: 'main',
|
|
221
|
-
|
|
296
|
+
...(token && { personalAccessToken: token })
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const historyManifest = await historyRes.json();
|
|
302
|
+
const localHistoryDir = path.join(artPath, 'history/local/main');
|
|
303
|
+
const remoteHistoryDir = path.join(artPath, 'history/remote/main');
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Replay every commit in chronological order to sync local state with
|
|
307
|
+
* remote history.
|
|
308
|
+
*/
|
|
222
309
|
|
|
223
|
-
|
|
310
|
+
for (const commitHash of historyManifest.commits) {
|
|
311
|
+
const commitRes = await fetch(
|
|
312
|
+
`${ARTIFACT_HOST}/commit`,
|
|
313
|
+
{
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify({
|
|
317
|
+
handle,
|
|
318
|
+
repo,
|
|
319
|
+
branch: 'main',
|
|
320
|
+
hash: commitHash,
|
|
321
|
+
...(token && { personalAccessToken: token })
|
|
224
322
|
})
|
|
225
|
-
}
|
|
226
|
-
|
|
323
|
+
}
|
|
324
|
+
);
|
|
227
325
|
|
|
228
326
|
const commitMaster = await commitRes.json();
|
|
229
327
|
|
|
230
328
|
let fullChanges = {};
|
|
231
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Fetch and merge paginated changes if the commit is split into parts.
|
|
332
|
+
*/
|
|
333
|
+
|
|
232
334
|
if (commitMaster.parts && commitMaster.parts.length > 0) {
|
|
233
335
|
for (const partName of commitMaster.parts) {
|
|
234
|
-
const partRes = await fetch(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
336
|
+
const partRes = await fetch(
|
|
337
|
+
`${ARTIFACT_HOST}/part`,
|
|
338
|
+
{
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: { 'Content-Type': 'application/json' },
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
type: 'history',
|
|
343
|
+
handle,
|
|
344
|
+
repo,
|
|
345
|
+
branch: 'main',
|
|
346
|
+
partName,
|
|
347
|
+
|
|
348
|
+
...(token && { personalAccessToken: token })
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
);
|
|
239
352
|
|
|
240
353
|
const partData = await partRes.json();
|
|
354
|
+
|
|
241
355
|
fs.writeFileSync(path.join(localHistoryDir, partName), JSON.stringify(partData, null, 2));
|
|
242
356
|
fs.writeFileSync(path.join(remoteHistoryDir, partName), JSON.stringify(partData, null, 2));
|
|
243
357
|
|
|
@@ -252,18 +366,24 @@ async function clone (repoSlug, providedToken = null) {
|
|
|
252
366
|
fs.writeFileSync(path.join(localHistoryDir, `${commitHash}.json`), masterContent);
|
|
253
367
|
fs.writeFileSync(path.join(remoteHistoryDir, `${commitHash}.json`), masterContent);
|
|
254
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Apply individual file operations (insert, delete, create) to the working tree.
|
|
371
|
+
*/
|
|
372
|
+
|
|
255
373
|
for (const [filePath, changeSet] of Object.entries(fullChanges)) {
|
|
256
374
|
const fullPath = path.join(targetPath, filePath);
|
|
257
375
|
|
|
258
376
|
if (Array.isArray(changeSet)) {
|
|
259
377
|
let content = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : '';
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
378
|
+
|
|
379
|
+
for (const operation of changeSet) {
|
|
380
|
+
if (operation.type === 'insert') {
|
|
381
|
+
content = content.slice(0, operation.position) + operation.content + content.slice(operation.position);
|
|
382
|
+
} else if (operation.type === 'delete') {
|
|
383
|
+
content = content.slice(0, operation.position) + content.slice(operation.position + operation.length);
|
|
265
384
|
}
|
|
266
385
|
}
|
|
386
|
+
|
|
267
387
|
fs.writeFileSync(fullPath, content);
|
|
268
388
|
} else if (changeSet.type === 'createFile') {
|
|
269
389
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
@@ -274,6 +394,10 @@ async function clone (repoSlug, providedToken = null) {
|
|
|
274
394
|
}
|
|
275
395
|
}
|
|
276
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Finalize the manifests and set the active parent pointer to the latest commit.
|
|
399
|
+
*/
|
|
400
|
+
|
|
277
401
|
const manifestJson = JSON.stringify({ commits: historyManifest.commits }, null, 2);
|
|
278
402
|
|
|
279
403
|
fs.writeFileSync(path.join(localHistoryDir, 'manifest.json'), manifestJson);
|
|
@@ -294,18 +418,29 @@ async function clone (repoSlug, providedToken = null) {
|
|
|
294
418
|
|
|
295
419
|
/**
|
|
296
420
|
* Updates the configuration in art.json.
|
|
421
|
+
* @param {string} key - Configuration key.
|
|
422
|
+
* @param {any} value - Configuration value.
|
|
423
|
+
* @returns {Object} Updated configuration object.
|
|
297
424
|
*/
|
|
298
|
-
|
|
425
|
+
|
|
426
|
+
function config (key, value) {
|
|
427
|
+
/**
|
|
428
|
+
* Verify repo existence and update specific configuration fields.
|
|
429
|
+
*/
|
|
430
|
+
|
|
299
431
|
const manifestPath = path.join(process.cwd(), '.art', 'art.json');
|
|
300
432
|
|
|
301
433
|
if (!fs.existsSync(manifestPath)) {
|
|
302
434
|
throw new Error('No art repository found.');
|
|
303
435
|
}
|
|
304
436
|
|
|
305
|
-
const manifest = JSON.parse(
|
|
437
|
+
const manifest = JSON.parse(
|
|
438
|
+
fs.readFileSync(manifestPath, 'utf8')
|
|
439
|
+
);
|
|
306
440
|
|
|
307
441
|
if (key && value !== undefined) {
|
|
308
442
|
manifest.configuration[key] = value;
|
|
443
|
+
|
|
309
444
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
310
445
|
}
|
|
311
446
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console colors.
|
|
3
|
+
* ANSI colors for both dark and light theme terminals.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const RED = '\x1b[31m';
|
|
7
|
+
export const GREEN = '\x1b[32m';
|
|
8
|
+
export const RESET = '\x1b[0m';
|
|
9
|
+
export const GRAY = '\x1b[90m';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Artifact configuration
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const MAX_PART_SIZE = 32000000;
|
|
@@ -1,87 +1,144 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Artifact - Modern version control.
|
|
3
|
+
* @author Benny Schmidt (https://github.com/bennyschmidt)
|
|
4
|
+
* @project https://github.com/bennyschmidt/artifact
|
|
5
|
+
* Module: Utils / getStateByHash (v0.3.3)
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
const fs = require('fs');
|
|
7
9
|
const path = require('path');
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
12
|
+
* Reconstructs the full file state of a branch at a specific point in time by replaying commits.
|
|
13
|
+
* @param {string} branchName - The branch to read history from.
|
|
14
|
+
* @param {string} targetHash - The commit hash representing the desired state.
|
|
15
|
+
* @returns {Object} A map of file paths to their full string content.
|
|
11
16
|
*/
|
|
12
17
|
|
|
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
|
-
|
|
18
|
+
function getStateByHash (branchName, targetHash) {
|
|
19
|
+
/**
|
|
20
|
+
* Initialize repository paths and verify the existence of the root manifest.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const artifactPath = path.join(process.cwd(), '.art');
|
|
24
|
+
const rootPath = path.join(artifactPath, 'root/manifest.json');
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(rootPath)) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load the initial "root" state which serves as the base
|
|
32
|
+
* for all subsequent deltas.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const rootMaster = JSON.parse(
|
|
36
|
+
fs.readFileSync(rootPath, 'utf8')
|
|
37
|
+
);
|
|
38
|
+
const branchPath = path.join(artifactPath, 'history/local', branchName);
|
|
39
|
+
const manifestPath = path.join(branchPath, 'manifest.json');
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(manifestPath)) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const manifest = JSON.parse(
|
|
46
|
+
fs.readFileSync(manifestPath, 'utf8')
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
let state = {};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Populate the state object with the original snapshots
|
|
53
|
+
* from the assembled root manifest parts.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
for (const partName of rootMaster.parts) {
|
|
57
|
+
const partPath = path.join(artifactPath, 'root', partName);
|
|
58
|
+
const partData = JSON.parse(
|
|
59
|
+
fs.readFileSync(partPath, 'utf8')
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
for (const file of partData.files) {
|
|
63
|
+
state[file.path] = file.content;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!targetHash) {
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replay the history of the branch chronologically,
|
|
73
|
+
* applying changes until the target hash is reached.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
for (const hash of manifest.commits) {
|
|
77
|
+
const commitPath = path.join(branchPath, `${hash}.json`);
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(commitPath)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const commitMaster = JSON.parse(
|
|
84
|
+
fs.readFileSync(commitPath, 'utf8')
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
let fullChanges = {};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Consolidate changes from commit parts or the master commit file.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
if (commitMaster.parts && Array.isArray(commitMaster.parts)) {
|
|
94
|
+
for (const partName of commitMaster.parts) {
|
|
95
|
+
const partPath = path.join(branchPath, partName);
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(partPath)) {
|
|
98
|
+
const partData = JSON.parse(
|
|
99
|
+
fs.readFileSync(partPath, 'utf8')
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
Object.assign(fullChanges, partData.changes);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (commitMaster.changes) {
|
|
106
|
+
fullChanges = commitMaster.changes;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Apply line-based operations (insert/delete)
|
|
111
|
+
* or file-level operations to the current state.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
for (const [filePath, changeSet] of Object.entries(fullChanges)) {
|
|
115
|
+
if (Array.isArray(changeSet)) {
|
|
116
|
+
let currentContent = state[filePath] || '';
|
|
117
|
+
|
|
118
|
+
for (const operation of changeSet) {
|
|
119
|
+
if (operation.type === 'insert') {
|
|
120
|
+
currentContent = `${currentContent.slice(0, operation.position)}${operation.content}${currentContent.slice(operation.position)}`;
|
|
121
|
+
} else if (operation.type === 'delete') {
|
|
122
|
+
currentContent = `${currentContent.slice(0, operation.position)}${currentContent.slice(operation.position + operation.length)}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
state[filePath] = currentContent;
|
|
127
|
+
} else {
|
|
128
|
+
if (changeSet.type === 'createFile') {
|
|
129
|
+
state[filePath] = changeSet.content;
|
|
130
|
+
} else if (changeSet.type === 'deleteFile') {
|
|
131
|
+
delete state[filePath];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (hash === targetHash) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = getStateByHash;
|