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.
@@ -1,51 +1,84 @@
1
1
  /**
2
- * art - Modern version control.
3
- * Module: Branching (v0.3.2)
2
+ * Artifact - Modern version control.
3
+ * @author Benny Schmidt (https://github.com/bennyschmidt)
4
+ * @project https://github.com/bennyschmidt/artifact
5
+ * Module: Branching (v0.3.3)
4
6
  */
5
7
 
6
8
  const fs = require('fs');
7
9
  const path = require('path');
8
10
 
9
11
  const getStateByHash = require('../utils/getStateByHash');
10
-
11
- const MAX_PART_SIZE = 32000000;
12
+ const { MAX_PART_SIZE } = require('../utils/constants');
12
13
 
13
14
  /**
14
- * Lists, creates, or deletes branches.
15
+ * Lists all existing branches, creates a new branch from the current HEAD,
16
+ * or deletes an existing branch from the local and remote history.
17
+ * * @param {Object} options - The branch options.
18
+ * @param {string} options.name - The name of the branch to create or delete.
19
+ * @param {boolean} options.isDelete - Whether the operation is a deletion.
20
+ * @returns {string|string[]} - A message or an array of branch names.
15
21
  */
16
22
 
17
23
  function branch ({ name, isDelete = false } = {}) {
18
- const artPath = path.join(process.cwd(), '.art');
19
- const localHistoryPath = path.join(artPath, 'history/local');
20
- const remoteHistoryPath = path.join(artPath, 'history/remote');
21
- const artJsonPath = path.join(artPath, 'art.json');
24
+ /**
25
+ * Define core paths for the .art directory and internal history structures.
26
+ */
27
+
28
+ const artifactPath = path.join(process.cwd(), '.art');
29
+ const localHistoryPath = path.join(artifactPath, 'history/local');
30
+ const remoteHistoryPath = path.join(artifactPath, 'history/remote');
31
+ const artifactJsonPath = path.join(artifactPath, 'art.json');
32
+
33
+ /**
34
+ * If no branch name is provided, return a list of all local branches.
35
+ * Filters out system files like .DS_Store or thumbs.db.
36
+ */
22
37
 
23
38
  if (!name) {
24
- return fs.readdirSync(localHistoryPath).filter(f => {
25
- return f !== '.DS_Store' && f !== 'desktop.ini' && f !== 'thumbs.db';
26
- });
39
+ const branchList = [];
40
+ const entries = fs.readdirSync(localHistoryPath);
41
+
42
+ for (const entry of entries) {
43
+ if (entry !== '.DS_Store' && entry !== 'desktop.ini' && entry !== 'thumbs.db') {
44
+ branchList.push(entry);
45
+ }
46
+ }
47
+
48
+ return branchList;
27
49
  }
28
50
 
51
+ /**
52
+ * Validate the branch name against illegal characters and naming patterns.
53
+ */
54
+
29
55
  const normalizedName = name.toLowerCase();
30
- const illegalRegExp = /[\/\\]/g;
31
- const controlRegExp = /[\x00-\x1f\x80-\x9f]/g;
32
- const reservedRegExp = /^\.+$/;
56
+ const illegalRegularExpression = /[\/\\]/g;
57
+ const controlRegularExpression = /[\x00-\x1f\x80-\x9f]/g;
58
+ const reservedRegularExpression = /^\.+$/;
33
59
 
34
- if (illegalRegExp.test(name) || controlRegExp.test(name) || reservedRegExp.test(name)) {
60
+ if (illegalRegularExpression.test(name) || controlRegularExpression.test(name) || reservedRegularExpression.test(name)) {
35
61
  throw new Error(`Invalid branch name: "${name}". Branch names cannot contain slashes or illegal characters.`);
36
62
  }
37
63
 
38
64
  const branchLocalPath = path.join(localHistoryPath, name);
39
65
  const branchRemotePath = path.join(remoteHistoryPath, name);
40
66
 
67
+ /**
68
+ * Pass `isDelete: true` to delete the branch in question.
69
+ * This is automatically set to true with CLI flags: --delete, -d, -D.
70
+ */
71
+
41
72
  if (isDelete) {
42
73
  if (!fs.existsSync(branchLocalPath)) {
43
74
  throw new Error(`Local branch "${name}" does not exist.`);
44
75
  }
45
76
 
46
- const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
77
+ const artifactJson = JSON.parse(
78
+ fs.readFileSync(artifactJsonPath, 'utf8')
79
+ );
47
80
 
48
- if (artJson.active.branch === name) {
81
+ if (artifactJson.active.branch === name) {
49
82
  throw new Error(`Local branch "${name}" is in use and can't be deleted right now.`);
50
83
  }
51
84
 
@@ -58,20 +91,37 @@ function branch ({ name, isDelete = false } = {}) {
58
91
  return `Deleted local branch "${name}".`;
59
92
  }
60
93
 
94
+ /**
95
+ * Check if the branch already exists before attempting creation.
96
+ */
97
+
61
98
  if (fs.existsSync(branchLocalPath)) {
62
99
  throw new Error(`Local branch "${name}" already exists.`);
63
100
  }
64
101
 
65
- const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
66
- const sourceBranchName = artJson.active.branch;
102
+ /**
103
+ * Read the active branch manifest to determine the starting commit history.
104
+ */
105
+
106
+ const artifactJson = JSON.parse(
107
+ fs.readFileSync(artifactJsonPath, 'utf8')
108
+ );
109
+
110
+ const sourceBranchName = artifactJson.active.branch;
67
111
  const currentBranchManifest = path.join(localHistoryPath, sourceBranchName, 'manifest.json');
68
112
 
69
113
  let initialCommits = [];
70
114
 
71
115
  if (fs.existsSync(currentBranchManifest)) {
72
- initialCommits = JSON.parse(fs.readFileSync(currentBranchManifest, 'utf8')).commits;
116
+ initialCommits = JSON.parse(
117
+ fs.readFileSync(currentBranchManifest, 'utf8')
118
+ ).commits;
73
119
  }
74
120
 
121
+ /**
122
+ * Create the local branch directory and initialize the manifest file.
123
+ */
124
+
75
125
  fs.mkdirSync(branchLocalPath, { recursive: true });
76
126
 
77
127
  fs.writeFileSync(
@@ -79,6 +129,11 @@ function branch ({ name, isDelete = false } = {}) {
79
129
  JSON.stringify({ commits: initialCommits }, null, 2)
80
130
  );
81
131
 
132
+ /**
133
+ * Initialize the remote tracking directory for the new branch
134
+ * (if it doesn't exist).
135
+ */
136
+
82
137
  if (!fs.existsSync(branchRemotePath)) {
83
138
  fs.mkdirSync(branchRemotePath, { recursive: true });
84
139
 
@@ -88,31 +143,37 @@ function branch ({ name, isDelete = false } = {}) {
88
143
  );
89
144
  }
90
145
 
146
+ /**
147
+ * Copy all commit data and files from the source branch to the new branch.
148
+ */
149
+
91
150
  if (initialCommits.length > 0) {
92
151
  const sourceBranchPath = path.join(localHistoryPath, sourceBranchName);
93
152
  const sourceRemotePath = path.join(remoteHistoryPath, sourceBranchName);
94
153
 
95
154
  for (const hash of initialCommits) {
96
155
  let masterFile = path.join(sourceBranchPath, `${hash}.json`);
97
- let currentSrcDir = sourceBranchPath;
156
+ let currentSourceDirectory = sourceBranchPath;
98
157
 
99
158
  if (!fs.existsSync(masterFile)) {
100
159
  masterFile = path.join(sourceRemotePath, `${hash}.json`);
101
- currentSrcDir = sourceRemotePath;
160
+ currentSourceDirectory = sourceRemotePath;
102
161
  }
103
162
 
104
163
  if (fs.existsSync(masterFile)) {
105
164
  fs.copyFileSync(masterFile, path.join(branchLocalPath, `${hash}.json`));
106
165
 
107
- const commitMaster = JSON.parse(fs.readFileSync(masterFile, 'utf8'));
166
+ const commitMaster = JSON.parse(
167
+ fs.readFileSync(masterFile, 'utf8')
168
+ );
108
169
 
109
170
  if (commitMaster.parts && Array.isArray(commitMaster.parts)) {
110
171
  for (const partName of commitMaster.parts) {
111
- const srcPart = path.join(currentSrcDir, partName);
112
- const destPart = path.join(branchLocalPath, partName);
172
+ const sourcePart = path.join(currentSourceDirectory, partName);
173
+ const destinationPart = path.join(branchLocalPath, partName);
113
174
 
114
- if (fs.existsSync(srcPart)) {
115
- fs.copyFileSync(srcPart, destPart);
175
+ if (fs.existsSync(sourcePart)) {
176
+ fs.copyFileSync(sourcePart, destinationPart);
116
177
  }
117
178
  }
118
179
  }
@@ -124,29 +185,52 @@ function branch ({ name, isDelete = false } = {}) {
124
185
  }
125
186
 
126
187
  /**
127
- * Updates the active pointer and reconstructs the working directory.
188
+ * Updates the active branch pointer and reconstructs the working directory
189
+ * based on the state of the target branch's latest commit.
190
+ * * @param {string} branchName - The name of the branch to switch to.
191
+ * @param {Object} options - Checkout options.
192
+ * @param {boolean} options.force - Whether to ignore local changes.
193
+ * @returns {string} - Success message.
128
194
  */
129
195
 
130
196
  function checkout (branchName, { force = false } = {}) {
197
+ /**
198
+ * Setup paths and ensure the target branch exists, creating it if necessary.
199
+ */
200
+
131
201
  const root = process.cwd();
132
- const artPath = path.join(root, '.art');
133
- const artJsonPath = path.join(artPath, 'art.json');
134
- const branchPath = path.join(artPath, 'history/local', branchName);
202
+ const artifactPath = path.join(root, '.art');
203
+ const artifactJsonPath = path.join(artifactPath, 'art.json');
204
+ const branchPath = path.join(artifactPath, 'history/local', branchName);
135
205
 
136
206
  if (!fs.existsSync(branchPath)) {
137
207
  branch({ name: branchName });
138
208
  }
139
209
 
140
- const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
141
- const currentState = getStateByHash(artJson.active.branch, artJson.active.parent) || {};
210
+ /**
211
+ * Retrieve current state to check for uncommitted changes.
212
+ */
213
+
214
+ const artifactJson = JSON.parse(
215
+ fs.readFileSync(artifactJsonPath, 'utf8')
216
+ );
217
+
218
+ const currentState = getStateByHash(artifactJson.active.branch, artifactJson.active.parent) || {};
219
+
220
+ /**
221
+ * Verify if the working directory is dirty.
222
+ * Throws an error unless `force: true` is passed.
223
+ */
142
224
 
143
225
  if (!force) {
144
- const allWorkDirFiles = fs.readdirSync(root, { recursive: true })
145
- .filter(f => !f.startsWith('.art') && !fs.statSync(path.join(root, f)).isDirectory());
226
+ const allWorkingDirectoryFiles = fs.readdirSync(root, { recursive: true })
227
+ .filter(file => {
228
+ return !file.startsWith('.art') && !fs.statSync(path.join(root, file)).isDirectory();
229
+ });
146
230
 
147
231
  let isDirty = false;
148
232
 
149
- for (const file of allWorkDirFiles) {
233
+ for (const file of allWorkingDirectoryFiles) {
150
234
  const currentContent = fs.readFileSync(path.join(root, file), 'utf8');
151
235
 
152
236
  if (currentContent !== currentState[file]) {
@@ -171,7 +255,13 @@ function checkout (branchName, { force = false } = {}) {
171
255
  }
172
256
  }
173
257
 
174
- const manifest = JSON.parse(fs.readFileSync(path.join(branchPath, 'manifest.json'), 'utf8'));
258
+ /**
259
+ * Identify the latest commit hash for the target branch.
260
+ */
261
+
262
+ const manifest = JSON.parse(
263
+ fs.readFileSync(path.join(branchPath, 'manifest.json'), 'utf8')
264
+ );
175
265
 
176
266
  const targetHash = manifest.commits.length > 0
177
267
  ? manifest.commits[manifest.commits.length - 1]
@@ -179,6 +269,10 @@ function checkout (branchName, { force = false } = {}) {
179
269
 
180
270
  const targetState = getStateByHash(branchName, targetHash);
181
271
 
272
+ /**
273
+ * Clean up files that exist in the current state but not in the target state.
274
+ */
275
+
182
276
  for (const filePath of Object.keys(currentState)) {
183
277
  if (!targetState[filePath]) {
184
278
  const fullPath = path.join(root, filePath);
@@ -189,6 +283,10 @@ function checkout (branchName, { force = false } = {}) {
189
283
  }
190
284
  }
191
285
 
286
+ /**
287
+ * Write target state files to the working directory.
288
+ */
289
+
192
290
  for (const [filePath, content] of Object.entries(targetState)) {
193
291
  const fullPath = path.join(root, filePath);
194
292
 
@@ -196,108 +294,166 @@ function checkout (branchName, { force = false } = {}) {
196
294
  fs.writeFileSync(fullPath, content);
197
295
  }
198
296
 
199
- artJson.active.branch = branchName;
200
- artJson.active.parent = targetHash;
201
- fs.writeFileSync(artJsonPath, JSON.stringify(artJson, null, 2));
297
+ /**
298
+ * Update the active branch and parent pointers in art.json.
299
+ */
300
+
301
+ artifactJson.active.branch = branchName;
302
+ artifactJson.active.parent = targetHash;
303
+ fs.writeFileSync(artifactJsonPath, JSON.stringify(artifactJson, null, 2));
202
304
 
203
305
  return `Switched to branch "${branchName}".`;
204
306
  }
205
307
 
206
308
  /**
207
- * Performs a three-way merge. Overwrites working directory with conflicts.
309
+ * Performs a three-way merge between the active branch and the target branch.
310
+ * If conflicts are detected, the working directory is updated with markers.
311
+ * * @param {string} targetBranch - The branch to merge into the active branch.
312
+ * @returns {string} - Success message.
208
313
  */
209
314
 
210
- function merge (targetBranch) {
211
- const root = process.cwd();
212
- const artPath = path.join(root, '.art');
213
- const stageDir = path.join(artPath, 'stage');
214
- const artJson = JSON.parse(fs.readFileSync(path.join(artPath, 'art.json'), 'utf8'));
215
- const activeBranch = artJson.active.branch;
315
+ function merge (targetBranch) {
316
+ /**
317
+ * Initialize staging environment and load manifests for the merge operation.
318
+ */
216
319
 
217
- if (fs.existsSync(stageDir)) {
218
- fs.rmSync(stageDir, { recursive: true, force: true });
219
- }
320
+ const root = process.cwd();
321
+ const artifactPath = path.join(root, '.art');
322
+ const stageDirectory = path.join(artifactPath, 'stage');
323
+ const artifactJson = JSON.parse(
324
+ fs.readFileSync(path.join(artifactPath, 'art.json'), 'utf8')
325
+ );
326
+ const activeBranch = artifactJson.active.branch;
327
+
328
+ if (fs.existsSync(stageDirectory)) {
329
+ fs.rmSync(stageDirectory, { recursive: true, force: true });
330
+ }
331
+
332
+ fs.mkdirSync(stageDirectory, { recursive: true });
333
+
334
+ const activeManifest = JSON.parse(
335
+ fs.readFileSync(path.join(artifactPath, `history/local/${activeBranch}/manifest.json`), 'utf8')
336
+ );
337
+ const targetManifest = JSON.parse(
338
+ fs.readFileSync(path.join(artifactPath, `history/local/${targetBranch}/manifest.json`), 'utf8')
339
+ );
340
+
341
+ /**
342
+ * Locate the most recent common ancestor hash between both branches.
343
+ */
344
+
345
+ const commonAncestorHash = [...activeManifest.commits].reverse().find(hash => {
346
+ return targetManifest.commits.includes(hash);
347
+ }) || null;
348
+
349
+ /**
350
+ * Capture the file states for the base (ancestor), active, and target branches.
351
+ */
352
+
353
+ const baseState = commonAncestorHash ? getStateByHash(activeBranch, commonAncestorHash) : {};
354
+ const activeState = getStateByHash(activeBranch, artifactJson.active.parent);
355
+ const lastTargetHash = targetManifest.commits[targetManifest.commits.length - 1];
356
+ const targetState = getStateByHash(targetBranch, lastTargetHash);
357
+ const allFiles = new Set([...Object.keys(activeState), ...Object.keys(targetState)]);
358
+
359
+ /**
360
+ * Prepare the staging mechanism for multi-part change tracking.
361
+ */
220
362
 
221
- fs.mkdirSync(stageDir, { recursive: true });
363
+ let currentPartChanges = {};
364
+ let currentPartSize = 0;
365
+ let partCount = 0;
222
366
 
223
- const activeManifest = JSON.parse(fs.readFileSync(path.join(artPath, `history/local/${activeBranch}/manifest.json`), 'utf8'));
224
- const targetManifest = JSON.parse(fs.readFileSync(path.join(artPath, `history/local/${targetBranch}/manifest.json`), 'utf8'));
367
+ const saveStagePart = () => {
368
+ if (Object.keys(currentPartChanges).length === 0) {
369
+ return;
370
+ }
225
371
 
226
- const commonAncestorHash = [...activeManifest.commits].reverse().find(h => targetManifest.commits.includes(h)) || null;
372
+ const partPath = path.join(stageDirectory, `part.${partCount}.json`);
227
373
 
228
- const baseState = commonAncestorHash ? getStateByHash(activeBranch, commonAncestorHash) : {};
229
- const activeState = getStateByHash(activeBranch, artJson.active.parent);
230
- const lastTargetHash = targetManifest.commits[targetManifest.commits.length - 1];
231
- const targetState = getStateByHash(targetBranch, lastTargetHash);
374
+ fs.writeFileSync(partPath, JSON.stringify({ changes: currentPartChanges }, null, 2));
375
+ currentPartChanges = {};
376
+ currentPartSize = 0;
377
+ partCount++;
378
+ };
232
379
 
233
- const allFiles = new Set([...Object.keys(activeState), ...Object.keys(targetState)]);
380
+ /**
381
+ * Iterate through all unique files to determine merge actions.
382
+ */
234
383
 
235
- let currentPartChanges = {};
236
- let currentPartSize = 0;
237
- let partCount = 0;
384
+ for (const filePath of allFiles) {
385
+ const base = baseState[filePath];
386
+ const active = activeState[filePath];
387
+ const target = targetState[filePath];
388
+ const fullPath = path.join(root, filePath);
238
389
 
239
- const saveStagePart = () => {
240
- if (Object.keys(currentPartChanges).length === 0) return;
390
+ if (active === target) {
391
+ continue;
392
+ }
241
393
 
242
- const partPath = path.join(stageDir, `part.${partCount}.json`);
394
+ let change = null;
243
395
 
244
- fs.writeFileSync(partPath, JSON.stringify({ changes: currentPartChanges }, null, 2));
245
- currentPartChanges = {};
246
- currentPartSize = 0;
247
- partCount++;
248
- };
396
+ /**
397
+ * Logic for applying target changes if they don't conflict with active
398
+ * local modifications.
399
+ */
249
400
 
250
- for (const filePath of allFiles) {
251
- const base = baseState[filePath];
252
- const active = activeState[filePath];
253
- const target = targetState[filePath];
254
- const fullPath = path.join(root, filePath);
401
+ if (base === active && base !== target) {
402
+ if (target === undefined) {
403
+ if (fs.existsSync(fullPath)) {
404
+ fs.unlinkSync(fullPath);
405
+ }
255
406
 
256
- if (active === target) continue;
407
+ change = { type: 'deleteFile' };
408
+ } else {
409
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
410
+ fs.writeFileSync(fullPath, target);
411
+ change = { type: 'createFile', content: target };
412
+ }
413
+ } else if (base !== active && base !== target && active !== target) {
414
+ /**
415
+ * Handle three-way merge conflicts by injecting markers into the target file.
416
+ */
257
417
 
258
- let change = null;
418
+ const conflictContent = `<<<<<<< active\n${active || ''}\n=======\n${target || ''}\n>>>>>>> ${targetBranch}`;
259
419
 
260
- if (base === active && base !== target) {
261
- if (target === undefined) {
262
- if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
263
- change = { type: 'deleteFile' };
264
- } else {
265
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
266
- fs.writeFileSync(fullPath, target);
267
- change = { type: 'createFile', content: target };
268
- }
269
- } else if (base !== active && base !== target && active !== target) {
270
- const conflictContent = `<<<<<<< active\n${active || ''}\n=======\n${target || ''}\n>>>>>>> ${targetBranch}`;
420
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
421
+ fs.writeFileSync(fullPath, conflictContent);
422
+ change = { type: 'createFile', content: conflictContent };
423
+ }
271
424
 
272
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
273
- fs.writeFileSync(fullPath, conflictContent);
274
- change = { type: 'createFile', content: conflictContent };
275
- }
425
+ /**
426
+ * Calculate change size and partition the stage.
427
+ */
276
428
 
277
- if (change) {
278
- const changeSize = JSON.stringify(change).length;
429
+ if (change) {
430
+ const changeSize = JSON.stringify(change).length;
279
431
 
280
- if (currentPartSize + changeSize > MAX_PART_SIZE) {
281
- saveStagePart();
282
- }
432
+ if (currentPartSize + changeSize > MAX_PART_SIZE) {
433
+ saveStagePart();
434
+ }
283
435
 
284
- currentPartChanges[filePath] = change;
285
- currentPartSize += changeSize;
286
- }
287
- }
436
+ currentPartChanges[filePath] = change;
437
+ currentPartSize += changeSize;
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Finalize the merge by saving the last stage part and the manifest summary.
443
+ */
288
444
 
289
- saveStagePart();
445
+ saveStagePart();
290
446
 
291
- fs.writeFileSync(
292
- path.join(stageDir, 'manifest.json'),
293
- JSON.stringify({ parts: Array.from({ length: partCount }, (_, i) => `part.${i}.json`) }, null, 2)
294
- );
447
+ fs.writeFileSync(
448
+ path.join(stageDirectory, 'manifest.json'),
449
+ JSON.stringify({ parts: Array.from({ length: partCount }, (_, index) => `part.${index}.json`) }, null, 2)
450
+ );
295
451
 
296
- return `Merged ${targetBranch}.`;
297
- }
452
+ return `Merged ${targetBranch}.`;
453
+ }
298
454
 
299
455
  module.exports = {
300
- __libraryVersion: '0.3.2',
456
+ __libraryVersion: '0.3.3',
301
457
  __libraryAPIName: 'Branching',
302
458
  branch,
303
459
  checkout,