pagan-artifact 0.2.8 → 0.3.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/workflow/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * art - Modern version control.
3
- * Module: Workflow (v0.2.8)
3
+ * Module: Workflow (v0.3.0)
4
4
  */
5
5
 
6
6
  const fs = require('fs');
@@ -10,16 +10,44 @@ const crypto = require('crypto');
10
10
  const getStateByHash = require('../utils/getStateByHash');
11
11
  const shouldIgnore = require('../utils/shouldIgnore');
12
12
 
13
+ const MAX_PART_SIZE = 32000000;
14
+
15
+ /**
16
+ * Helper to load all changes from a paginated stage directory.
17
+ */
18
+
19
+ function getStagedChanges(artPath) {
20
+ const stageDir = path.join(artPath, 'stage');
21
+ const manifestPath = path.join(stageDir, 'manifest.json');
22
+
23
+ if (!fs.existsSync(stageDir) || !fs.existsSync(manifestPath)) {
24
+ return {};
25
+ }
26
+
27
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
28
+ let allChanges = {};
29
+
30
+ for (const partName of manifest.parts) {
31
+ const partPath = path.join(stageDir, partName);
32
+
33
+ if (fs.existsSync(partPath)) {
34
+ const partData = JSON.parse(fs.readFileSync(partPath, 'utf8'));
35
+
36
+ Object.assign(allChanges, partData.changes);
37
+ }
38
+ }
39
+
40
+ return allChanges;
41
+ }
42
+
13
43
  /**
14
44
  * Compares the working directory against the last commit and pending stage.
15
45
  */
16
46
 
17
- function status() {
47
+ function status () {
18
48
  const root = process.cwd();
19
49
  const artPath = path.join(root, '.art');
20
50
  const artJsonPath = path.join(artPath, 'art.json');
21
- const shouldIgnore = require('../utils/shouldIgnore');
22
- const getStateByHash = require('../utils/getStateByHash');
23
51
 
24
52
  if (!fs.existsSync(artJsonPath)) {
25
53
  throw new Error('No art repository found.');
@@ -27,13 +55,8 @@ const shouldIgnore = require('../utils/shouldIgnore');
27
55
 
28
56
  const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
29
57
  const activeBranch = artJson.active.branch;
30
- const stagePath = path.join(artPath, 'stage.json');
31
-
32
- let stagedFiles = {};
33
- if (fs.existsSync(stagePath)) {
34
- stagedFiles = JSON.parse(fs.readFileSync(stagePath, 'utf8')).changes;
35
- }
36
58
 
59
+ const stagedFiles = getStagedChanges(artPath);
37
60
  const activeState = getStateByHash(activeBranch, artJson.active.parent) || {};
38
61
 
39
62
  const allFiles = fs.readdirSync(root, { recursive: true })
@@ -47,14 +70,13 @@ const shouldIgnore = require('../utils/shouldIgnore');
47
70
  const isStaged = !!stagedFiles[file];
48
71
  const isActive = !!activeState[file];
49
72
 
50
- if (file === '.art' || file.startsWith(`.art${path.sep}`)) {
51
- continue;
52
- }
73
+ if (file === '.art' || file.startsWith(`.art${path.sep}`)) continue;
53
74
 
54
75
  const isIgnored = shouldIgnore(file);
55
76
 
56
77
  if (isIgnored && !isActive && !isStaged) {
57
78
  ignored.push(file);
79
+
58
80
  continue;
59
81
  }
60
82
 
@@ -80,175 +102,202 @@ const shouldIgnore = require('../utils/shouldIgnore');
80
102
  }
81
103
 
82
104
  /**
83
- * Updates or creates a JSON diff in the stage.json file.
84
- * Implements character-precise position tracking.
105
+ * Updates or creates a JSON diff in the stage directory.
85
106
  */
86
107
 
87
- function add (targetPath) {
88
- const root = process.cwd();
89
- const artPath = path.join(root, '.art');
90
- const stagePath = path.join(artPath, 'stage.json');
91
- const artJsonPath = path.join(artPath, 'art.json');
92
- const fullPath = path.resolve(root, targetPath);
93
-
94
- if (!fs.existsSync(fullPath)) {
95
- throw new Error(`Path does not exist: ${targetPath}`);
96
- }
97
-
98
- const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
99
- const activeState = getStateByHash(artJson.active.branch, artJson.active.parent) || {};
100
-
101
- const stats = fs.statSync(fullPath);
102
- const relativeTarget = path.relative(root, fullPath);
103
-
104
- if (!stats.isDirectory() && shouldIgnore(relativeTarget) && !activeState[relativeTarget]) {
105
- return `${relativeTarget} is being ignored. Edit your .artignore file to remove it.`;
106
- }
107
-
108
- let stage = { changes: {} };
109
- if (fs.existsSync(stagePath)) {
110
- stage = JSON.parse(fs.readFileSync(stagePath, 'utf8'));
111
- }
112
-
113
- let filesToProcess = [];
114
-
115
- if (stats.isDirectory()) {
116
- filesToProcess = fs.readdirSync(fullPath, { recursive: true })
117
- .filter(f => {
118
- const absoluteF = path.join(fullPath, f);
119
- const relF = path.relative(root, absoluteF);
120
- const isDir = fs.statSync(absoluteF).isDirectory();
121
-
122
- if (relF.startsWith('.art') || relF.includes(`${path.sep}.art`)) return false;
123
-
124
- const isTracked = !!activeState[relF];
125
- const isIgnored = shouldIgnore(relF);
126
-
127
- return !isDir && (!isIgnored || isTracked);
128
- })
129
- .map(f => path.relative(root, path.join(fullPath, f)));
130
- } else {
131
- filesToProcess = [relativeTarget];
132
- }
133
-
134
- if (filesToProcess.length === 0) {
135
- return "No changes to add.";
136
- }
137
-
138
- for (const relPath of filesToProcess) {
139
- const currentContent = fs.readFileSync(path.join(root, relPath), 'utf8');
140
- const previousContent = activeState[relPath];
141
-
142
- if (previousContent === undefined) {
143
- stage.changes[relPath] = {
144
- type: 'createFile',
145
- content: currentContent
146
- };
147
-
148
- continue;
149
- }
150
-
151
- if (currentContent !== previousContent) {
152
- const operations = [];
153
- let start = 0;
154
-
155
- while (start < previousContent.length && start < currentContent.length && previousContent[start] === currentContent[start]) {
156
- start++;
157
- }
158
-
159
- let oldEnd = previousContent.length - 1;
160
- let newEnd = currentContent.length - 1;
161
-
162
- while (oldEnd >= start && newEnd >= start && previousContent[oldEnd] === currentContent[newEnd]) {
163
- oldEnd--;
164
- newEnd--;
165
- }
166
-
167
- const deletionLength = oldEnd - start + 1;
168
-
169
- if (deletionLength > 0) {
170
- operations.push({
171
- type: 'delete',
172
- position: start,
173
- length: deletionLength
174
- });
175
- }
176
-
177
- const insertionContent = currentContent.slice(start, newEnd + 1);
178
- if (insertionContent.length > 0) {
179
- operations.push({
180
- type: 'insert',
181
- position: start,
182
- content: insertionContent
183
- });
184
- }
185
-
186
- if (operations.length > 0) {
187
- stage.changes[relPath] = operations;
188
- }
189
- }
190
- }
191
-
192
- fs.writeFileSync(stagePath, JSON.stringify(stage, null, 2));
193
-
194
- return `Added ${filesToProcess.length} file(s) to stage.`;
195
- }
108
+ function add (targetPath) {
109
+ const root = process.cwd();
110
+ const artPath = path.join(root, '.art');
111
+ const stageDir = path.join(artPath, 'stage');
112
+ const artJsonPath = path.join(artPath, 'art.json');
113
+ const fullPath = path.resolve(root, targetPath);
114
+
115
+ if (!fs.existsSync(fullPath)) {
116
+ throw new Error(`Path does not exist: ${targetPath}`);
117
+ }
118
+
119
+ const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
120
+ const activeState = getStateByHash(artJson.active.branch, artJson.active.parent) || {};
121
+ const currentStaged = getStagedChanges(artPath);
122
+
123
+ const stats = fs.statSync(fullPath);
124
+ const relativeTarget = path.relative(root, fullPath);
125
+
126
+ if (!stats.isDirectory() && shouldIgnore(relativeTarget) && !activeState[relativeTarget]) {
127
+ return `${relativeTarget} is being ignored.`;
128
+ }
129
+
130
+ let filesToProcess = [];
131
+
132
+ if (stats.isDirectory()) {
133
+ filesToProcess = fs.readdirSync(fullPath, { recursive: true })
134
+ .filter(f => {
135
+ const absF = path.join(fullPath, f);
136
+ const relF = path.relative(root, absF);
137
+
138
+ return !fs.statSync(absF).isDirectory() && !relF.startsWith('.art') && (!shouldIgnore(relF) || !!activeState[relF]);
139
+ })
140
+ .map(f => path.relative(root, path.join(fullPath, f)));
141
+ } else {
142
+ filesToProcess = [relativeTarget];
143
+ }
144
+
145
+ if (filesToProcess.length === 0) return "No changes to add.";
146
+
147
+ for (const relPath of filesToProcess) {
148
+ const currentContent = fs.readFileSync(path.join(root, relPath), 'utf8');
149
+ const previousContent = activeState[relPath];
150
+
151
+ if (previousContent === undefined) {
152
+ currentStaged[relPath] = { type: 'createFile', content: currentContent };
153
+ } else if (currentContent !== previousContent) {
154
+ let start = 0;
155
+
156
+ while (start < previousContent.length && start < currentContent.length && previousContent[start] === currentContent[start]) {
157
+ start++;
158
+ }
159
+
160
+ let oldEnd = previousContent.length - 1;
161
+ let newEnd = currentContent.length - 1;
162
+
163
+ while (oldEnd >= start && newEnd >= start && previousContent[oldEnd] === currentContent[newEnd]) {
164
+ oldEnd--; newEnd--;
165
+ }
166
+
167
+ const operations = [];
168
+ const deletionLength = oldEnd - start + 1;
169
+
170
+ if (deletionLength > 0) {
171
+ operations.push({ type: 'delete', position: start, length: deletionLength });
172
+ }
173
+
174
+ const insertionContent = currentContent.slice(start, newEnd + 1);
175
+
176
+ if (insertionContent.length > 0) {
177
+ operations.push({ type: 'insert', position: start, content: insertionContent });
178
+ }
179
+
180
+ if (operations.length > 0) {
181
+ currentStaged[relPath] = operations;
182
+ }
183
+ }
184
+ }
185
+
186
+ if (fs.existsSync(stageDir)) fs.rmSync(stageDir, { recursive: true, force: true });
187
+
188
+ fs.mkdirSync(stageDir, { recursive: true });
189
+
190
+ const stageParts = [];
191
+
192
+ let currentPartChanges = {};
193
+ let currentSize = 0;
194
+
195
+ const savePart = () => {
196
+ const partName = `part.${stageParts.length}.json`;
197
+
198
+ fs.writeFileSync(path.join(stageDir, partName), JSON.stringify({ changes: currentPartChanges }, null, 2));
199
+ stageParts.push(partName);
200
+ currentPartChanges = {};
201
+ currentSize = 0;
202
+ };
203
+
204
+ for (const [file, changes] of Object.entries(currentStaged)) {
205
+ const size = JSON.stringify(changes).length;
206
+
207
+ if (currentSize + size > MAX_PART_SIZE && Object.keys(currentPartChanges).length > 0) {
208
+ savePart();
209
+ }
210
+
211
+ currentPartChanges[file] = changes;
212
+ currentSize += size;
213
+ }
214
+
215
+ savePart();
216
+
217
+ fs.writeFileSync(path.join(stageDir, 'manifest.json'), JSON.stringify({ parts: stageParts }, null, 2));
218
+
219
+ return `Added ${filesToProcess.length} file(s) to paginated stage.`;
220
+ }
196
221
 
197
222
  /**
198
- * Finalizes the stage into a commit file.
223
+ * Finalizes the paginated stage into a paginated commit structure.
199
224
  */
200
225
 
201
226
  function commit (message) {
202
- if (!message) {
203
- throw new Error('A commit message is required.');
204
- }
227
+ if (!message) throw new Error('A commit message is required.');
205
228
 
229
+ const MAX_PART_SIZE = 32000000;
206
230
  const artPath = path.join(process.cwd(), '.art');
207
- const stagePath = path.join(artPath, 'stage.json');
231
+ const stageDir = path.join(artPath, 'stage');
208
232
  const artJsonPath = path.join(artPath, 'art.json');
209
233
 
210
- if (!fs.existsSync(stagePath)) {
211
- throw new Error('Nothing to commit (stage is empty).');
212
- }
234
+ if (!fs.existsSync(stageDir)) throw new Error('Nothing to commit (stage is empty).');
213
235
 
214
- const stage = JSON.parse(fs.readFileSync(stagePath, 'utf8'));
236
+ const stagedChanges = getStagedChanges(artPath);
215
237
  const artJson = JSON.parse(fs.readFileSync(artJsonPath, 'utf8'));
216
238
  const branch = artJson.active.branch;
217
239
  const timestamp = Date.now();
218
240
 
219
- const hash = crypto
241
+ const commitHash = crypto
220
242
  .createHash('sha1')
221
- .update(JSON.stringify(stage.changes) + timestamp + message)
243
+ .update(JSON.stringify(stagedChanges) + timestamp + message)
222
244
  .digest('hex');
223
245
 
224
- const commitObject = {
225
- hash,
246
+ const branchHistoryDir = path.join(artPath, 'history', 'local', branch);
247
+ const commitParts = [];
248
+
249
+ let currentPartChanges = {};
250
+ let currentPartSize = 0;
251
+
252
+ const saveCommitPart = () => {
253
+ if (Object.keys(currentPartChanges).length === 0) return;
254
+
255
+ const partName = `${commitHash}.part.${commitParts.length}.json`;
256
+
257
+ fs.writeFileSync(path.join(branchHistoryDir, partName), JSON.stringify({ changes: currentPartChanges }, null, 2));
258
+ commitParts.push(partName);
259
+
260
+ currentPartChanges = {};
261
+ currentPartSize = 0;
262
+ };
263
+
264
+ for (const [filePath, changeSet] of Object.entries(stagedChanges)) {
265
+ const changeSize = JSON.stringify(changeSet).length;
266
+
267
+ if (currentPartSize + changeSize > MAX_PART_SIZE && Object.keys(currentPartChanges).length > 0) {
268
+ saveCommitPart();
269
+ }
270
+
271
+ currentPartChanges[filePath] = changeSet;
272
+ currentPartSize += changeSize;
273
+ }
274
+ saveCommitPart();
275
+
276
+ const commitMaster = {
277
+ hash: commitHash,
226
278
  message,
227
279
  timestamp,
228
280
  parent: artJson.active.parent,
229
- changes: stage.changes
281
+ parts: commitParts
230
282
  };
231
283
 
232
- const branchHistoryDir = path.join(artPath, 'history', 'local', branch);
233
- const commitFilePath = path.join(branchHistoryDir, `${hash}.json`);
234
- const manifestPath = path.join(branchHistoryDir, 'manifest.json');
284
+ fs.writeFileSync(path.join(branchHistoryDir, `${commitHash}.json`), JSON.stringify(commitMaster, null, 2));
235
285
 
236
- fs.writeFileSync(commitFilePath, JSON.stringify(commitObject, null, 2));
286
+ const manifest = JSON.parse(fs.readFileSync(path.join(branchHistoryDir, 'manifest.json'), 'utf8'));
237
287
 
238
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
288
+ manifest.commits.push(commitHash);
289
+ fs.writeFileSync(path.join(branchHistoryDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
239
290
 
240
- manifest.commits.push(hash);
241
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
242
-
243
- artJson.active.parent = hash;
291
+ artJson.active.parent = commitHash;
244
292
  fs.writeFileSync(artJsonPath, JSON.stringify(artJson, null, 2));
245
- fs.unlinkSync(stagePath);
246
293
 
247
- return `[${branch} ${hash.slice(0, 7)}] ${message}`;
294
+ fs.rmSync(stageDir, { recursive: true, force: true });
295
+
296
+ return `[${branch} ${commitHash.slice(0, 7)}] ${message} (${commitParts.length} parts)`;
248
297
  }
249
298
 
250
299
  module.exports = {
251
- __libraryVersion: '0.2.8',
300
+ __libraryVersion: '0.3.0',
252
301
  __libraryAPIName: 'Workflow',
253
302
  status,
254
303
  add,