pagan-artifact 0.3.2 → 0.3.4
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 +205 -53
- 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/caches/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: Caches (v0.3.4)
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
const fs = require('fs');
|
|
@@ -8,29 +10,40 @@ const path = require('path');
|
|
|
8
10
|
|
|
9
11
|
const { checkout } = require('../branching/index.js');
|
|
10
12
|
const getStateByHash = require('../utils/getStateByHash');
|
|
11
|
-
|
|
12
|
-
const MAX_PART_SIZE = 32000000;
|
|
13
|
+
const { MAX_PART_SIZE } = require('../utils/constants');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Helper to load all changes from a paginated directory (Stage or Stash).
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
function getPaginatedChanges (
|
|
19
|
-
|
|
19
|
+
function getPaginatedChanges (directoryPath) {
|
|
20
|
+
/**
|
|
21
|
+
* Locate the manifest file to determine how many parts exist in the cache.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const manifestPath = path.join(directoryPath, 'manifest.json');
|
|
20
25
|
|
|
21
|
-
if (!fs.existsSync(
|
|
26
|
+
if (!fs.existsSync(directoryPath) || !fs.existsSync(manifestPath)) {
|
|
22
27
|
return {};
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
const manifest = JSON.parse(
|
|
30
|
+
const manifest = JSON.parse(
|
|
31
|
+
fs.readFileSync(manifestPath, 'utf8')
|
|
32
|
+
);
|
|
26
33
|
|
|
27
34
|
let allChanges = {};
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Iterate through the parts defined in the manifest and merge them into a single object.
|
|
38
|
+
*/
|
|
39
|
+
|
|
29
40
|
for (const partName of manifest.parts) {
|
|
30
|
-
const partPath = path.join(
|
|
41
|
+
const partPath = path.join(directoryPath, partName);
|
|
31
42
|
|
|
32
43
|
if (fs.existsSync(partPath)) {
|
|
33
|
-
const partData = JSON.parse(
|
|
44
|
+
const partData = JSON.parse(
|
|
45
|
+
fs.readFileSync(partPath, 'utf8')
|
|
46
|
+
);
|
|
34
47
|
|
|
35
48
|
Object.assign(allChanges, partData.changes);
|
|
36
49
|
}
|
|
@@ -43,29 +56,47 @@ function getPaginatedChanges (dirPath) {
|
|
|
43
56
|
* Helper to write changes to a paginated directory.
|
|
44
57
|
*/
|
|
45
58
|
|
|
46
|
-
function savePaginatedChanges (
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
function savePaginatedChanges (directoryPath, changes) {
|
|
60
|
+
/**
|
|
61
|
+
* Clear any existing cache data at the target path before writing new parts.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync(directoryPath)) {
|
|
65
|
+
fs.rmSync(directoryPath, { recursive: true, force: true });
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
fs.mkdirSync(
|
|
68
|
+
fs.mkdirSync(directoryPath, { recursive: true });
|
|
52
69
|
|
|
53
70
|
const parts = [];
|
|
54
71
|
|
|
55
72
|
let currentPartChanges = {};
|
|
56
73
|
let currentSize = 0;
|
|
57
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Internal helper to serialize the current change set to a JSON file.
|
|
77
|
+
*/
|
|
78
|
+
|
|
58
79
|
const savePart = () => {
|
|
59
|
-
if (Object.keys(currentPartChanges).length === 0)
|
|
80
|
+
if (Object.keys(currentPartChanges).length === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
60
83
|
|
|
61
84
|
const partName = `part.${parts.length}.json`;
|
|
62
85
|
|
|
63
|
-
fs.writeFileSync(
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(directoryPath, partName),
|
|
88
|
+
JSON.stringify({ changes: currentPartChanges }, null, 2)
|
|
89
|
+
);
|
|
90
|
+
|
|
64
91
|
parts.push(partName);
|
|
65
92
|
currentPartChanges = {};
|
|
66
93
|
currentSize = 0;
|
|
67
94
|
};
|
|
68
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Distribute changes across multiple files if they exceed the MAX_PART_SIZE.
|
|
98
|
+
*/
|
|
99
|
+
|
|
69
100
|
for (const [file, changeSet] of Object.entries(changes)) {
|
|
70
101
|
const size = JSON.stringify(changeSet).length;
|
|
71
102
|
|
|
@@ -78,48 +109,112 @@ function savePaginatedChanges (dirPath, changes) {
|
|
|
78
109
|
}
|
|
79
110
|
|
|
80
111
|
savePart();
|
|
81
|
-
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Write the final manifest so the reader knows which parts to load.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
path.join(directoryPath, 'manifest.json'),
|
|
119
|
+
JSON.stringify({ parts }, null, 2)
|
|
120
|
+
);
|
|
82
121
|
}
|
|
83
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Saves current local changes to a temporary storage or restores the latest stash.
|
|
125
|
+
* @param {Object} options - Stash options.
|
|
126
|
+
* @param {boolean} options.pop - Whether to restore and remove the latest stash.
|
|
127
|
+
* @param {boolean} options.list - Whether to list all existing stashes.
|
|
128
|
+
* @returns {string|Object[]} - Status message or list of stash objects.
|
|
129
|
+
*/
|
|
130
|
+
|
|
84
131
|
function stash ({ pop = false, list = false } = {}) {
|
|
132
|
+
/**
|
|
133
|
+
* Define root and artifact paths required for stash operations.
|
|
134
|
+
*/
|
|
135
|
+
|
|
85
136
|
const root = process.cwd();
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const cachePath = path.join(
|
|
89
|
-
const
|
|
137
|
+
const artifactPath = path.join(root, '.art');
|
|
138
|
+
const stageDirectory = path.join(artifactPath, 'stage');
|
|
139
|
+
const cachePath = path.join(artifactPath, 'cache');
|
|
140
|
+
const artifactJsonPath = path.join(artifactPath, 'art.json');
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Retrieve and format a list of all saved stashes if the list flag is active.
|
|
144
|
+
*/
|
|
90
145
|
|
|
91
146
|
if (list) {
|
|
92
|
-
if (!fs.existsSync(cachePath))
|
|
147
|
+
if (!fs.existsSync(cachePath)) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const stashDirectories = [];
|
|
152
|
+
const entries = fs.readdirSync(cachePath);
|
|
153
|
+
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const entryPath = path.join(cachePath, entry);
|
|
156
|
+
if (entry.startsWith('stash_') && fs.statSync(entryPath).isDirectory()) {
|
|
157
|
+
stashDirectories.push(entry);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stashDirectories.sort();
|
|
162
|
+
|
|
163
|
+
const formattedStashes = [];
|
|
93
164
|
|
|
94
|
-
const
|
|
95
|
-
.
|
|
96
|
-
|
|
165
|
+
for (const [index, directoryName] of stashDirectories.entries()) {
|
|
166
|
+
formattedStashes.push({
|
|
167
|
+
id: `stash@{${stashDirectories.length - 1 - index}}`,
|
|
168
|
+
date: new Date(
|
|
169
|
+
parseInt(directoryName.replace('stash_', ''))
|
|
170
|
+
).toLocaleString(),
|
|
171
|
+
directoryName
|
|
172
|
+
});
|
|
173
|
+
}
|
|
97
174
|
|
|
98
|
-
return
|
|
99
|
-
id: `stash@{${stashDirs.length - 1 - index}}`,
|
|
100
|
-
date: new Date(parseInt(dirName.replace('stash_', ''))).toLocaleString(),
|
|
101
|
-
dirName
|
|
102
|
-
}));
|
|
175
|
+
return formattedStashes;
|
|
103
176
|
}
|
|
104
177
|
|
|
178
|
+
/**
|
|
179
|
+
* If `pop: true`, restore the most recent stash and delete its cache.
|
|
180
|
+
*/
|
|
181
|
+
|
|
105
182
|
if (pop) {
|
|
106
|
-
if (!fs.existsSync(cachePath))
|
|
183
|
+
if (!fs.existsSync(cachePath)) {
|
|
184
|
+
throw new Error('No stashes found.');
|
|
185
|
+
}
|
|
107
186
|
|
|
108
|
-
const stashes =
|
|
109
|
-
|
|
110
|
-
.sort();
|
|
187
|
+
const stashes = [];
|
|
188
|
+
const entries = fs.readdirSync(cachePath);
|
|
111
189
|
|
|
112
|
-
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const entryPath = path.join(cachePath, entry);
|
|
192
|
+
if (entry.startsWith('stash_') && fs.statSync(entryPath).isDirectory()) {
|
|
193
|
+
stashes.push(entry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
stashes.sort();
|
|
113
198
|
|
|
114
|
-
|
|
115
|
-
|
|
199
|
+
if (stashes.length === 0) {
|
|
200
|
+
throw new Error('No stashes found.');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const latestStashDirectoryName = stashes[stashes.length - 1];
|
|
204
|
+
const latestStashPath = path.join(cachePath, latestStashDirectoryName);
|
|
116
205
|
const stashChanges = getPaginatedChanges(latestStashPath);
|
|
117
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Apply the cached changes back to the working directory.
|
|
209
|
+
*/
|
|
210
|
+
|
|
118
211
|
for (const [filePath, changeSet] of Object.entries(stashChanges)) {
|
|
119
212
|
const fullPath = path.join(root, filePath);
|
|
120
213
|
|
|
121
214
|
if (Array.isArray(changeSet)) {
|
|
122
|
-
let content = fs.existsSync(fullPath)
|
|
215
|
+
let content = fs.existsSync(fullPath)
|
|
216
|
+
? fs.readFileSync(fullPath, 'utf8')
|
|
217
|
+
: '';
|
|
123
218
|
|
|
124
219
|
for (const operation of changeSet) {
|
|
125
220
|
if (operation.type === 'insert') {
|
|
@@ -134,23 +229,38 @@ function stash ({ pop = false, list = false } = {}) {
|
|
|
134
229
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
135
230
|
fs.writeFileSync(fullPath, changeSet.content);
|
|
136
231
|
} else if (changeSet.type === 'deleteFile') {
|
|
137
|
-
if (fs.existsSync(fullPath))
|
|
232
|
+
if (fs.existsSync(fullPath)) {
|
|
233
|
+
fs.unlinkSync(fullPath);
|
|
234
|
+
}
|
|
138
235
|
}
|
|
139
236
|
}
|
|
140
237
|
|
|
141
238
|
fs.rmSync(latestStashPath, { recursive: true, force: true });
|
|
142
|
-
|
|
239
|
+
|
|
240
|
+
return `Restored changes from ${latestStashDirectoryName}.`;
|
|
143
241
|
}
|
|
144
242
|
|
|
145
|
-
|
|
146
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Create a new stash by comparing the workdir to the last commit.
|
|
245
|
+
*/
|
|
147
246
|
|
|
148
|
-
const
|
|
149
|
-
.
|
|
247
|
+
const artifactJson = JSON.parse(
|
|
248
|
+
fs.readFileSync(artifactJsonPath, 'utf8')
|
|
249
|
+
);
|
|
250
|
+
const activeState = getStateByHash(artifactJson.active.branch, artifactJson.active.parent) || {};
|
|
251
|
+
|
|
252
|
+
const allWorkingDirectoryFiles = fs.readdirSync(root, { recursive: true })
|
|
253
|
+
.filter(file => {
|
|
254
|
+
return !file.startsWith('.art') && !fs.statSync(path.join(root, file)).isDirectory();
|
|
255
|
+
});
|
|
150
256
|
|
|
151
257
|
const stashChanges = {};
|
|
152
258
|
|
|
153
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Diff existing files, handling both text and binary content.
|
|
261
|
+
*/
|
|
262
|
+
|
|
263
|
+
for (const file of allWorkingDirectoryFiles) {
|
|
154
264
|
const fullPath = path.join(root, file);
|
|
155
265
|
const currentBuffer = fs.readFileSync(fullPath);
|
|
156
266
|
const isBinary = currentBuffer.includes(0);
|
|
@@ -159,7 +269,10 @@ function stash ({ pop = false, list = false } = {}) {
|
|
|
159
269
|
const previousContent = activeState[file];
|
|
160
270
|
|
|
161
271
|
if (previousContent === undefined) {
|
|
162
|
-
stashChanges[file] = {
|
|
272
|
+
stashChanges[file] = {
|
|
273
|
+
type: 'createFile',
|
|
274
|
+
content: isBinary ? currentBuffer.toString('base64') : currentContent
|
|
275
|
+
};
|
|
163
276
|
} else if (currentContent !== previousContent && !isBinary) {
|
|
164
277
|
let start = 0;
|
|
165
278
|
|
|
@@ -175,66 +288,106 @@ function stash ({ pop = false, list = false } = {}) {
|
|
|
175
288
|
newEnd--;
|
|
176
289
|
}
|
|
177
290
|
|
|
178
|
-
const
|
|
179
|
-
const
|
|
291
|
+
const operations = [];
|
|
292
|
+
const deletionLength = oldEnd - start + 1;
|
|
180
293
|
|
|
181
|
-
if (
|
|
294
|
+
if (deletionLength > 0) {
|
|
295
|
+
operations.push({ type: 'delete', position: start, length: deletionLength });
|
|
296
|
+
}
|
|
182
297
|
|
|
183
|
-
const
|
|
298
|
+
const insertionContent = currentContent.slice(start, newEnd + 1);
|
|
184
299
|
|
|
185
|
-
if (
|
|
300
|
+
if (insertionContent.length > 0) {
|
|
301
|
+
operations.push({ type: 'insert', position: start, content: insertionContent });
|
|
302
|
+
}
|
|
186
303
|
|
|
187
|
-
if (
|
|
188
|
-
stashChanges[file] =
|
|
304
|
+
if (operations.length > 0) {
|
|
305
|
+
stashChanges[file] = operations;
|
|
189
306
|
}
|
|
190
307
|
}
|
|
191
308
|
}
|
|
192
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Detect files that were deleted from the working directory.
|
|
312
|
+
*/
|
|
313
|
+
|
|
193
314
|
for (const file in activeState) {
|
|
194
315
|
if (!fs.existsSync(path.join(root, file))) {
|
|
195
316
|
stashChanges[file] = { type: 'deleteFile' };
|
|
196
317
|
}
|
|
197
318
|
}
|
|
198
319
|
|
|
199
|
-
if (Object.keys(stashChanges).length === 0)
|
|
320
|
+
if (Object.keys(stashChanges).length === 0) {
|
|
321
|
+
return 'No local changes to stash.';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Save the detected changes to the cache and revert the working directory.
|
|
326
|
+
*/
|
|
200
327
|
|
|
201
328
|
const timestamp = Date.now();
|
|
202
329
|
const newStashPath = path.join(cachePath, `stash_${timestamp}`);
|
|
203
330
|
|
|
204
331
|
savePaginatedChanges(newStashPath, stashChanges);
|
|
205
332
|
|
|
206
|
-
if (fs.existsSync(
|
|
207
|
-
fs.rmSync(
|
|
333
|
+
if (fs.existsSync(stageDirectory)) {
|
|
334
|
+
fs.rmSync(stageDirectory, { recursive: true, force: true });
|
|
208
335
|
}
|
|
209
336
|
|
|
210
|
-
checkout(
|
|
337
|
+
checkout(artifactJson.active.branch, { force: true });
|
|
211
338
|
|
|
212
|
-
return `
|
|
339
|
+
return `Stashed working directory changes and reverted to a clean state.`;
|
|
213
340
|
}
|
|
214
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Resets the active state to a specific commit.
|
|
344
|
+
* @param {string} hash - The commit hash to reset to.
|
|
345
|
+
* @returns {string} - Status message.
|
|
346
|
+
*/
|
|
347
|
+
|
|
215
348
|
function reset (hash) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Clear the staging area before performing a reset.
|
|
351
|
+
*/
|
|
219
352
|
|
|
220
|
-
|
|
221
|
-
|
|
353
|
+
const artifactPath = path.join(process.cwd(), '.art');
|
|
354
|
+
const stageDirectory = path.join(artifactPath, 'stage');
|
|
355
|
+
const artifactJsonPath = path.join(artifactPath, 'art.json');
|
|
356
|
+
|
|
357
|
+
if (fs.existsSync(stageDirectory)) {
|
|
358
|
+
fs.rmSync(stageDirectory, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!hash) {
|
|
362
|
+
return 'Staging area cleared.';
|
|
222
363
|
}
|
|
223
364
|
|
|
224
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Verify the existence of the commit hash within the active branch history.
|
|
367
|
+
*/
|
|
225
368
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
369
|
+
const artifactJson = JSON.parse(
|
|
370
|
+
fs.readFileSync(artifactJsonPath, 'utf8')
|
|
371
|
+
);
|
|
372
|
+
const branchName = artifactJson.active.branch;
|
|
373
|
+
const branchPath = path.join(artifactPath, 'history/local', branchName);
|
|
229
374
|
const commitPath = path.join(branchPath, `${hash}.json`);
|
|
230
375
|
|
|
231
|
-
if (!fs.existsSync(commitPath))
|
|
376
|
+
if (!fs.existsSync(commitPath)) {
|
|
377
|
+
throw new Error(`Commit ${hash} not found in branch ${branchName}.`);
|
|
378
|
+
}
|
|
232
379
|
|
|
233
|
-
|
|
234
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Update the active parent pointer and truncate the manifest commits.
|
|
382
|
+
*/
|
|
383
|
+
|
|
384
|
+
artifactJson.active.parent = hash;
|
|
385
|
+
fs.writeFileSync(artifactJsonPath, JSON.stringify(artifactJson, null, 2));
|
|
235
386
|
|
|
236
387
|
const manifestPath = path.join(branchPath, 'manifest.json');
|
|
237
|
-
const manifest = JSON.parse(
|
|
388
|
+
const manifest = JSON.parse(
|
|
389
|
+
fs.readFileSync(manifestPath, 'utf8')
|
|
390
|
+
);
|
|
238
391
|
const hashIndex = manifest.commits.indexOf(hash);
|
|
239
392
|
|
|
240
393
|
if (hashIndex !== -1) {
|
|
@@ -242,26 +395,53 @@ function reset (hash) {
|
|
|
242
395
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
243
396
|
}
|
|
244
397
|
|
|
245
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Reconstruct the working directory to match the reset commit state.
|
|
400
|
+
*/
|
|
401
|
+
|
|
402
|
+
checkout(branchName);
|
|
246
403
|
|
|
247
404
|
return `Branch is now at ${hash.slice(0, 7)}. Working directory updated.`;
|
|
248
405
|
}
|
|
249
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Removes a file from the working tree and stages the deletion.
|
|
409
|
+
* @param {string} filePath - Path to the file to be removed.
|
|
410
|
+
* @returns {string} - Status message.
|
|
411
|
+
*/
|
|
412
|
+
|
|
250
413
|
function rm (filePath) {
|
|
251
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Stage the file deletion by updating the paginated stage cache.
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
const artifactPath = path.join(process.cwd(), '.art');
|
|
252
419
|
const fullPath = path.join(process.cwd(), filePath);
|
|
253
|
-
|
|
420
|
+
|
|
421
|
+
const stage = getPaginatedChanges(
|
|
422
|
+
path.join(artifactPath, 'stage')
|
|
423
|
+
);
|
|
254
424
|
|
|
255
425
|
stage[filePath] = { type: 'deleteFile' };
|
|
256
|
-
savePaginatedChanges(path.join(artPath, 'stage'), stage);
|
|
257
426
|
|
|
258
|
-
|
|
427
|
+
savePaginatedChanges(
|
|
428
|
+
path.join(artifactPath, 'stage'),
|
|
429
|
+
stage
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Physically remove the file from the working directory if it exists.
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
if (fs.existsSync(fullPath)) {
|
|
437
|
+
fs.unlinkSync(fullPath);
|
|
438
|
+
}
|
|
259
439
|
|
|
260
440
|
return `File ${filePath} marked for removal.`;
|
|
261
441
|
}
|
|
262
442
|
|
|
263
443
|
module.exports = {
|
|
264
|
-
__libraryVersion: '0.3.
|
|
444
|
+
__libraryVersion: '0.3.4',
|
|
265
445
|
__libraryAPIName: 'Caches',
|
|
266
446
|
stash,
|
|
267
447
|
reset,
|