@dotinc/ogre 0.1.2 → 0.2.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/CHANGELOG.md +17 -0
- package/lib/commit.d.ts +3 -2
- package/lib/commit.js +3 -3
- package/lib/hash.d.ts +0 -0
- package/lib/hash.js +0 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/interfaces.d.ts +0 -0
- package/lib/interfaces.js +0 -0
- package/lib/ref.d.ts +0 -0
- package/lib/ref.js +0 -0
- package/lib/repository.d.ts +7 -0
- package/lib/repository.js +56 -42
- package/lib/size.d.ts +0 -0
- package/lib/size.js +0 -0
- package/lib/test.utils.d.ts +0 -0
- package/lib/test.utils.js +0 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +27 -0
- package/package.json +2 -5
- package/src/commit.test.ts +10 -0
- package/src/commit.ts +3 -2
- package/src/index.ts +1 -0
- package/src/repository.test.ts +12 -0
- package/src/repository.ts +49 -33
- package/src/utils.test.ts +44 -0
- package/src/utils.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
# v0.2.0 (Sun Mar 13 2022)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- Add visualization [#4](https://github.com/dotindustries/ogre/pull/4) ([@nadilas](https://github.com/nadilas))
|
|
6
|
+
- feat: add OgreGraph visualization ([@nadilas](https://github.com/nadilas))
|
|
7
|
+
|
|
8
|
+
#### ⚠️ Pushed to `main`
|
|
9
|
+
|
|
10
|
+
- chore: remove old dev deps ([@nadilas](https://github.com/nadilas))
|
|
11
|
+
|
|
12
|
+
#### Authors: 1
|
|
13
|
+
|
|
14
|
+
- [@nadilas](https://github.com/nadilas)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
1
18
|
# v0.1.2 (Fri Mar 11 2022)
|
|
2
19
|
|
|
3
20
|
#### 🐛 Bug Fix
|
package/lib/commit.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Change } from './interfaces';
|
|
2
2
|
export interface Commit {
|
|
3
3
|
hash: string;
|
|
4
|
+
tree: string;
|
|
4
5
|
message: string | undefined;
|
|
5
6
|
author: string;
|
|
6
7
|
parent: string | undefined;
|
|
@@ -8,11 +9,11 @@ export interface Commit {
|
|
|
8
9
|
timestamp: Date;
|
|
9
10
|
to: number;
|
|
10
11
|
}
|
|
11
|
-
export interface
|
|
12
|
+
export interface CommitHashContent {
|
|
12
13
|
message: string;
|
|
13
14
|
author: string;
|
|
14
15
|
parentRef: string | undefined;
|
|
15
16
|
changes: Change[];
|
|
16
17
|
timestamp: Date;
|
|
17
18
|
}
|
|
18
|
-
export declare function
|
|
19
|
+
export declare function calculateCommitHash(content: CommitHashContent): Promise<string>;
|
package/lib/commit.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.calculateCommitHash = void 0;
|
|
4
4
|
const hash_1 = require("./hash");
|
|
5
|
-
function
|
|
5
|
+
function calculateCommitHash(content) {
|
|
6
6
|
return (0, hash_1.digest)(content);
|
|
7
7
|
}
|
|
8
|
-
exports.
|
|
8
|
+
exports.calculateCommitHash = calculateCommitHash;
|
package/lib/hash.d.ts
CHANGED
|
File without changes
|
package/lib/hash.js
CHANGED
|
File without changes
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/interfaces.d.ts
CHANGED
|
File without changes
|
package/lib/interfaces.js
CHANGED
|
File without changes
|
package/lib/ref.d.ts
CHANGED
|
File without changes
|
package/lib/ref.js
CHANGED
|
File without changes
|
package/lib/repository.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { History } from './interfaces';
|
|
2
2
|
import { Commit } from './commit';
|
|
3
|
+
export declare const REFS_HEAD_KEY = "HEAD";
|
|
4
|
+
export declare const REFS_MAIN_KEY: string;
|
|
3
5
|
export interface RepositoryOptions<T> {
|
|
4
6
|
history?: History;
|
|
5
7
|
}
|
|
@@ -22,6 +24,11 @@ export interface RespositoryObjectType {
|
|
|
22
24
|
* A repository recording and managing the state transitions of an object
|
|
23
25
|
*/
|
|
24
26
|
export declare const Repository: RespositoryObjectType;
|
|
27
|
+
export declare const createHeadRefValue: (refKey: string) => string;
|
|
28
|
+
export declare const isTagRef: (refKey: string) => boolean;
|
|
29
|
+
export declare const cleanRefValue: (ref: string) => string;
|
|
30
|
+
export declare const brancheNameToRef: (name: string) => string;
|
|
31
|
+
export declare const validateBranchName: (name: string) => void;
|
|
25
32
|
/**
|
|
26
33
|
* Prints the underlying changelog of a repository
|
|
27
34
|
* @param repository
|
package/lib/repository.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.printChangeLog = exports.Repository = void 0;
|
|
3
|
+
exports.printChangeLog = exports.validateBranchName = exports.brancheNameToRef = exports.cleanRefValue = exports.isTagRef = exports.createHeadRefValue = exports.Repository = exports.REFS_MAIN_KEY = exports.REFS_HEAD_KEY = void 0;
|
|
4
4
|
const commit_1 = require("./commit");
|
|
5
5
|
const ref_1 = require("./ref");
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const hash_1 = require("./hash");
|
|
7
|
+
const tagRefPathPrefix = 'refs/tags/';
|
|
8
|
+
const headsRefPathPrefix = 'refs/heads/';
|
|
9
|
+
const headValueRefPrefix = 'ref: ';
|
|
10
|
+
exports.REFS_HEAD_KEY = 'HEAD';
|
|
11
|
+
exports.REFS_MAIN_KEY = `${headsRefPathPrefix}main`;
|
|
9
12
|
/**
|
|
10
13
|
* A repository recording and managing the state transitions of an object
|
|
11
14
|
*/
|
|
@@ -13,9 +16,9 @@ exports.Repository = function (obj, options) {
|
|
|
13
16
|
var _a, _b, _c, _d;
|
|
14
17
|
let savedLength;
|
|
15
18
|
let version = 0;
|
|
16
|
-
const refs = (_b = (_a = options.history) === null || _a === void 0 ? void 0 : _a.refs) !== null && _b !== void 0 ? _b : new Map([[
|
|
17
|
-
name:
|
|
18
|
-
value: `ref: ${
|
|
19
|
+
const refs = (_b = (_a = options.history) === null || _a === void 0 ? void 0 : _a.refs) !== null && _b !== void 0 ? _b : new Map([[exports.REFS_HEAD_KEY, {
|
|
20
|
+
name: exports.REFS_HEAD_KEY,
|
|
21
|
+
value: `ref: ${exports.REFS_MAIN_KEY}`
|
|
19
22
|
}]]);
|
|
20
23
|
const changeLog = [];
|
|
21
24
|
const targets = [];
|
|
@@ -105,28 +108,28 @@ exports.Repository = function (obj, options) {
|
|
|
105
108
|
this.data = new Proxy(obj, handler);
|
|
106
109
|
// region Read state read
|
|
107
110
|
this.head = () => {
|
|
108
|
-
const ref = refs.get(
|
|
111
|
+
const ref = refs.get(exports.REFS_HEAD_KEY);
|
|
109
112
|
if (!ref) {
|
|
110
113
|
throw new Error(`unreachable: HEAD is not present`);
|
|
111
114
|
}
|
|
112
|
-
return cleanRefValue(ref.value);
|
|
115
|
+
return (0, exports.cleanRefValue)(ref.value);
|
|
113
116
|
};
|
|
114
117
|
this.ref = (reference) => {
|
|
115
118
|
var _a;
|
|
116
119
|
const ref = (_a = refs.get(reference)) === null || _a === void 0 ? void 0 : _a.value;
|
|
117
|
-
return ref ? cleanRefValue(ref) : undefined;
|
|
120
|
+
return ref ? (0, exports.cleanRefValue)(ref) : undefined;
|
|
118
121
|
};
|
|
119
122
|
this.branch = () => {
|
|
120
|
-
const currentHeadRef = refs.get(
|
|
123
|
+
const currentHeadRef = refs.get(exports.REFS_HEAD_KEY);
|
|
121
124
|
if (!currentHeadRef) {
|
|
122
125
|
throw new Error('unreachable: ref HEAD not available');
|
|
123
126
|
}
|
|
124
|
-
if (currentHeadRef.value.includes(
|
|
125
|
-
const refName = cleanRefValue(currentHeadRef.value);
|
|
127
|
+
if (currentHeadRef.value.includes(headValueRefPrefix)) {
|
|
128
|
+
const refName = (0, exports.cleanRefValue)(currentHeadRef.value);
|
|
126
129
|
if (refs.has(refName))
|
|
127
130
|
return getLastItem(refName);
|
|
128
131
|
}
|
|
129
|
-
return
|
|
132
|
+
return exports.REFS_HEAD_KEY; // detached state
|
|
130
133
|
};
|
|
131
134
|
// endregion
|
|
132
135
|
// region History functions
|
|
@@ -176,7 +179,7 @@ exports.Repository = function (obj, options) {
|
|
|
176
179
|
};
|
|
177
180
|
// region Commit lookups
|
|
178
181
|
const commitAtHead = () => {
|
|
179
|
-
return
|
|
182
|
+
return commitAtRefIn(exports.REFS_HEAD_KEY, refs, commits);
|
|
180
183
|
};
|
|
181
184
|
const mustCommitAtHead = () => {
|
|
182
185
|
const commitHead = commitAtHead();
|
|
@@ -202,13 +205,14 @@ exports.Repository = function (obj, options) {
|
|
|
202
205
|
}
|
|
203
206
|
const timestamp = new Date();
|
|
204
207
|
const changes = [...changesSinceLastCommit];
|
|
205
|
-
const sha = await (0, commit_1.
|
|
208
|
+
const sha = await (0, commit_1.calculateCommitHash)({
|
|
206
209
|
message,
|
|
207
210
|
author,
|
|
208
211
|
changes,
|
|
209
212
|
parentRef: parent === null || parent === void 0 ? void 0 : parent.hash,
|
|
210
213
|
timestamp
|
|
211
214
|
});
|
|
215
|
+
const treeHash = await (0, hash_1.digest)(obj);
|
|
212
216
|
const commit = {
|
|
213
217
|
hash: sha,
|
|
214
218
|
message,
|
|
@@ -216,7 +220,8 @@ exports.Repository = function (obj, options) {
|
|
|
216
220
|
changes: changes,
|
|
217
221
|
parent: parent === null || parent === void 0 ? void 0 : parent.hash,
|
|
218
222
|
timestamp,
|
|
219
|
-
to: version
|
|
223
|
+
to: version,
|
|
224
|
+
tree: treeHash
|
|
220
225
|
};
|
|
221
226
|
if (amend) {
|
|
222
227
|
const idx = commits.findIndex(c => c === parent);
|
|
@@ -230,30 +235,30 @@ exports.Repository = function (obj, options) {
|
|
|
230
235
|
}
|
|
231
236
|
else {
|
|
232
237
|
// move detached HEAD to new commit
|
|
233
|
-
moveRef(
|
|
238
|
+
moveRef(exports.REFS_HEAD_KEY, commit);
|
|
234
239
|
}
|
|
235
240
|
return sha;
|
|
236
241
|
};
|
|
237
242
|
// region Graph manipulation
|
|
238
243
|
this.checkout = (shaish, createBranch = false) => {
|
|
239
244
|
if (createBranch) {
|
|
240
|
-
validateBranchName(shaish);
|
|
241
|
-
let branchRef = brancheNameToRef(shaish);
|
|
245
|
+
(0, exports.validateBranchName)(shaish);
|
|
246
|
+
let branchRef = (0, exports.brancheNameToRef)(shaish);
|
|
242
247
|
const commit = commitAtHead();
|
|
243
248
|
if (commit) {
|
|
244
249
|
moveRef(branchRef, commit);
|
|
245
250
|
}
|
|
246
|
-
moveRef(
|
|
251
|
+
moveRef(exports.REFS_HEAD_KEY, branchRef);
|
|
247
252
|
}
|
|
248
253
|
else {
|
|
249
254
|
const [commit, isRef, refKey] = shaishToCommit(shaish, refs, commits);
|
|
250
255
|
rebuildChangeLog(commit);
|
|
251
|
-
moveRef(
|
|
256
|
+
moveRef(exports.REFS_HEAD_KEY, isRef && refKey !== undefined ? refKey : commit);
|
|
252
257
|
}
|
|
253
258
|
};
|
|
254
259
|
this.createBranch = (name) => {
|
|
255
|
-
validateBranchName(name);
|
|
256
|
-
const refName = brancheNameToRef(name);
|
|
260
|
+
(0, exports.validateBranchName)(name);
|
|
261
|
+
const refName = (0, exports.brancheNameToRef)(name);
|
|
257
262
|
const headCommit = commitAtHead();
|
|
258
263
|
if (!headCommit) {
|
|
259
264
|
const headRef = this.head();
|
|
@@ -267,7 +272,7 @@ exports.Repository = function (obj, options) {
|
|
|
267
272
|
};
|
|
268
273
|
const moveRef = (refName, value) => {
|
|
269
274
|
let ref = refs.get(refName);
|
|
270
|
-
const val = typeof value === 'string' ?
|
|
275
|
+
const val = typeof value === 'string' ? (0, exports.createHeadRefValue)(value) : value.hash;
|
|
271
276
|
if (!ref) {
|
|
272
277
|
ref = { name: getLastItem(refName), value: val };
|
|
273
278
|
}
|
|
@@ -283,7 +288,7 @@ exports.Repository = function (obj, options) {
|
|
|
283
288
|
// for fancier merge tree
|
|
284
289
|
// https://github.com/isomorphic-git/isomorphic-git/blob/a623133345a5d8b6bb7a8352ea9702ce425d8266/src/utils/mergeTree.js#L33
|
|
285
290
|
if (typeof source !== 'string') {
|
|
286
|
-
// const srcHead =
|
|
291
|
+
// const srcHead = commitAtRefIn(REFS_HEAD, src.refs, src.commits)
|
|
287
292
|
throw new Error(`fatal: source type (${source instanceof exports.Repository ? 'Repository' : 'History'}) not implemented`);
|
|
288
293
|
}
|
|
289
294
|
const [srcCommit] = shaishToCommit(source, refs, commits);
|
|
@@ -331,15 +336,6 @@ exports.Repository = function (obj, options) {
|
|
|
331
336
|
}
|
|
332
337
|
};
|
|
333
338
|
const getLastItem = (thePath) => thePath.substring(thePath.lastIndexOf('/') + 1);
|
|
334
|
-
const cleanRefValue = (ref) => ref.replace(refPrefix, '');
|
|
335
|
-
const brancheNameToRef = (name) => {
|
|
336
|
-
return `refs/heads/${name}`;
|
|
337
|
-
};
|
|
338
|
-
const validateBranchName = (name) => {
|
|
339
|
-
if (!(0, ref_1.validBranch)(name)) {
|
|
340
|
-
throw new Error(`invalid ref name`);
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
339
|
/**
|
|
344
340
|
* Traverses the commit tree backwards and reassembles the changelog
|
|
345
341
|
* @param commit
|
|
@@ -370,14 +366,14 @@ const mapPath = (from, to, commits) => {
|
|
|
370
366
|
* @param references
|
|
371
367
|
* @param commitsList
|
|
372
368
|
*/
|
|
373
|
-
const
|
|
369
|
+
const commitAtRefIn = (ref, references, commitsList) => {
|
|
374
370
|
const reference = references.get(ref);
|
|
375
371
|
if (!reference) {
|
|
376
372
|
throw new Error(`unreachable: '${ref}' is not present`);
|
|
377
373
|
}
|
|
378
374
|
let commitHash;
|
|
379
|
-
if (reference.value.includes(
|
|
380
|
-
const refKey = cleanRefValue(reference.value);
|
|
375
|
+
if (reference.value.includes(headValueRefPrefix)) {
|
|
376
|
+
const refKey = (0, exports.cleanRefValue)(reference.value);
|
|
381
377
|
const targetRef = references.get(refKey);
|
|
382
378
|
if (!targetRef) {
|
|
383
379
|
// target branch may not have been saved yet
|
|
@@ -412,9 +408,9 @@ const shaishToCommit = (shaish, references, commitsList) => {
|
|
|
412
408
|
isRef = true;
|
|
413
409
|
refKey = name;
|
|
414
410
|
sha = ref.value;
|
|
415
|
-
if (sha.includes(
|
|
416
|
-
const cleanedRef = cleanRefValue(sha);
|
|
417
|
-
const c =
|
|
411
|
+
if (sha.includes(headValueRefPrefix)) {
|
|
412
|
+
const cleanedRef = (0, exports.cleanRefValue)(sha);
|
|
413
|
+
const c = commitAtRefIn(cleanedRef, references, commitsList);
|
|
418
414
|
if (!c) {
|
|
419
415
|
throw new Error(`${cleanedRef} points to non-existing commit`);
|
|
420
416
|
}
|
|
@@ -434,6 +430,24 @@ const shaishToCommit = (shaish, references, commitsList) => {
|
|
|
434
430
|
}
|
|
435
431
|
return [found[0], isRef, refKey];
|
|
436
432
|
};
|
|
433
|
+
const createHeadRefValue = (refKey) => {
|
|
434
|
+
return `${headValueRefPrefix}${refKey}`;
|
|
435
|
+
};
|
|
436
|
+
exports.createHeadRefValue = createHeadRefValue;
|
|
437
|
+
const isTagRef = (refKey) => refKey.indexOf(tagRefPathPrefix) > -1;
|
|
438
|
+
exports.isTagRef = isTagRef;
|
|
439
|
+
const cleanRefValue = (ref) => ref.replace(headValueRefPrefix, '');
|
|
440
|
+
exports.cleanRefValue = cleanRefValue;
|
|
441
|
+
const brancheNameToRef = (name) => {
|
|
442
|
+
return `${headsRefPathPrefix}${name}`;
|
|
443
|
+
};
|
|
444
|
+
exports.brancheNameToRef = brancheNameToRef;
|
|
445
|
+
const validateBranchName = (name) => {
|
|
446
|
+
if (!(0, ref_1.validBranch)(name)) {
|
|
447
|
+
throw new Error(`invalid ref name`);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
exports.validateBranchName = validateBranchName;
|
|
437
451
|
/**
|
|
438
452
|
* Prints the underlying changelog of a repository
|
|
439
453
|
* @param repository
|
|
@@ -442,7 +456,7 @@ const printChangeLog = (repository) => {
|
|
|
442
456
|
console.log('----------------------------------------------------------');
|
|
443
457
|
console.log(`Changelog at ${repository.head()}`);
|
|
444
458
|
const history = repository.getHistory();
|
|
445
|
-
const head =
|
|
459
|
+
const head = commitAtRefIn(repository.head(), history.refs, history.commits);
|
|
446
460
|
if (!head) {
|
|
447
461
|
throw new Error(`fatal: HEAD is not defined`);
|
|
448
462
|
}
|
package/lib/size.d.ts
CHANGED
|
File without changes
|
package/lib/size.js
CHANGED
|
File without changes
|
package/lib/test.utils.d.ts
CHANGED
|
File without changes
|
package/lib/test.utils.js
CHANGED
|
File without changes
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const cleanAuthor: (author: string) => [name: string, email: string];
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cleanAuthor = void 0;
|
|
4
|
+
// [RFC5322](https://www.ietf.org/rfc/rfc5322.txt)
|
|
5
|
+
const emailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
|
|
6
|
+
const cleanAuthor = (author) => {
|
|
7
|
+
if (author === '') {
|
|
8
|
+
throw new Error(`author not provided`);
|
|
9
|
+
}
|
|
10
|
+
// author name <email>
|
|
11
|
+
let strings = author.split(' <');
|
|
12
|
+
if (strings.length > 1) {
|
|
13
|
+
return [strings[0], strings[1].replace('>', '')];
|
|
14
|
+
}
|
|
15
|
+
// author name @handle
|
|
16
|
+
strings = author.split(' @');
|
|
17
|
+
if (strings.length > 1) {
|
|
18
|
+
return [strings[0], `@${strings[1]}`];
|
|
19
|
+
}
|
|
20
|
+
// email@domain.com
|
|
21
|
+
if (emailRegex.test(author)) {
|
|
22
|
+
return ['', author];
|
|
23
|
+
}
|
|
24
|
+
// unrecognized format
|
|
25
|
+
return [author, ''];
|
|
26
|
+
};
|
|
27
|
+
exports.cleanAuthor = cleanAuthor;
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "git",
|
|
5
5
|
"url": "https://github.com/dotindustries/ogre.git"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.2.0",
|
|
8
8
|
"description": "Git-like repository for in-memory object versioning",
|
|
9
9
|
"main": "lib/index.js",
|
|
10
10
|
"types": "lib/index.d.ts",
|
|
@@ -36,11 +36,8 @@
|
|
|
36
36
|
"@types/node": "^17.0.21",
|
|
37
37
|
"@types/uuid": "^8.3.4",
|
|
38
38
|
"ava": "^4.0.1",
|
|
39
|
-
"coveralls": "^3.1.1",
|
|
40
39
|
"nyc": "^15.1.0",
|
|
41
40
|
"ts-node": "^10.7.0",
|
|
42
|
-
"tslib": "^2.3.1",
|
|
43
|
-
"turbo": "^1.1.5",
|
|
44
41
|
"typescript": "^4.5.5",
|
|
45
42
|
"uuid": "^8.3.2"
|
|
46
43
|
},
|
|
@@ -48,5 +45,5 @@
|
|
|
48
45
|
"registry": "https://registry.npmjs.org/",
|
|
49
46
|
"access": "public"
|
|
50
47
|
},
|
|
51
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "fb4343e4c5f6cc2b87eb9204a643b6cd632ada69"
|
|
52
49
|
}
|
package/src/commit.test.ts
CHANGED
|
@@ -32,6 +32,16 @@ test('no commit without changes after recent commit', async t => {
|
|
|
32
32
|
return await repo.commit('baseline', testAuthor)
|
|
33
33
|
}, { message: 'no changes to commit' })
|
|
34
34
|
})
|
|
35
|
+
|
|
36
|
+
test('treeHash of commit is matching content', async t => {
|
|
37
|
+
const [repo] = await getBaseline()
|
|
38
|
+
repo.data.name = 'new name'
|
|
39
|
+
await repo.commit('baseline', testAuthor)
|
|
40
|
+
const {commits} = repo.getHistory()
|
|
41
|
+
t.is(commits.length, 1)
|
|
42
|
+
t.not(commits[0].tree, '', 'tree hash mismatch')
|
|
43
|
+
})
|
|
44
|
+
|
|
35
45
|
test('no commit --amend without commit', async t => {
|
|
36
46
|
const [repo] = await getBaseline()
|
|
37
47
|
repo.data.name = 'new name'
|
package/src/commit.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface Commit {
|
|
|
10
10
|
// - author commit timestamp with timezone
|
|
11
11
|
// - commit message
|
|
12
12
|
hash: string
|
|
13
|
+
tree: string
|
|
13
14
|
|
|
14
15
|
message: string | undefined
|
|
15
16
|
author: string
|
|
@@ -32,7 +33,7 @@ export interface Commit {
|
|
|
32
33
|
to: number;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export interface
|
|
36
|
+
export interface CommitHashContent {
|
|
36
37
|
message: string
|
|
37
38
|
author: string
|
|
38
39
|
parentRef: string | undefined
|
|
@@ -40,6 +41,6 @@ export interface HashContent {
|
|
|
40
41
|
timestamp: Date
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
export function
|
|
44
|
+
export function calculateCommitHash(content: CommitHashContent) {
|
|
44
45
|
return digest(content)
|
|
45
46
|
}
|
package/src/index.ts
CHANGED
package/src/repository.test.ts
CHANGED
|
@@ -24,3 +24,15 @@ test('reconstruction', async t => {
|
|
|
24
24
|
t.is(history2.commits.length, 2, 'incorrect # of commits')
|
|
25
25
|
t.is(sumChanges(history2.commits), changeEntries, 'incorrect # of changelog entries')
|
|
26
26
|
})
|
|
27
|
+
|
|
28
|
+
test('history contains HEAD ref', async t => {
|
|
29
|
+
const [repo] = await getBaseline()
|
|
30
|
+
|
|
31
|
+
t.is(repo.head(), 'refs/heads/main')
|
|
32
|
+
|
|
33
|
+
const history = repo.getHistory()
|
|
34
|
+
let headRef = history.refs.get('HEAD')
|
|
35
|
+
t.not(headRef, undefined)
|
|
36
|
+
t.is(headRef!.name, 'HEAD')
|
|
37
|
+
t.is(headRef!.value, 'ref: refs/heads/main')
|
|
38
|
+
})
|
package/src/repository.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Change, History, Reference } from './interfaces'
|
|
2
|
-
import {
|
|
2
|
+
import { calculateCommitHash, Commit } from './commit'
|
|
3
3
|
import { validBranch } from './ref'
|
|
4
|
+
import {digest} from './hash'
|
|
5
|
+
|
|
6
|
+
const tagRefPathPrefix = 'refs/tags/'
|
|
7
|
+
const headsRefPathPrefix = 'refs/heads/'
|
|
8
|
+
const headValueRefPrefix = 'ref: '
|
|
9
|
+
|
|
10
|
+
export const REFS_HEAD_KEY = 'HEAD'
|
|
11
|
+
export const REFS_MAIN_KEY = `${headsRefPathPrefix}main`
|
|
4
12
|
|
|
5
|
-
const REFS_HEAD = 'HEAD'
|
|
6
|
-
const REFS_MAIN = 'refs/heads/main'
|
|
7
|
-
const refPrefix = 'ref: '
|
|
8
13
|
|
|
9
14
|
export interface RepositoryOptions<T> {
|
|
10
15
|
history?: History
|
|
@@ -49,9 +54,9 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
49
54
|
) {
|
|
50
55
|
let savedLength: number | undefined
|
|
51
56
|
let version = 0
|
|
52
|
-
const refs: Map<string, Reference> = options.history?.refs ?? new Map<string, Reference>([[
|
|
53
|
-
name:
|
|
54
|
-
value: `ref: ${
|
|
57
|
+
const refs: Map<string, Reference> = options.history?.refs ?? new Map<string, Reference>([[REFS_HEAD_KEY, {
|
|
58
|
+
name: REFS_HEAD_KEY,
|
|
59
|
+
value: `ref: ${REFS_MAIN_KEY}`
|
|
55
60
|
}]])
|
|
56
61
|
const changeLog: Change[] = []
|
|
57
62
|
const targets: any[] = []
|
|
@@ -149,7 +154,7 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
149
154
|
|
|
150
155
|
// region Read state read
|
|
151
156
|
this.head = () => {
|
|
152
|
-
const ref = refs.get(
|
|
157
|
+
const ref = refs.get(REFS_HEAD_KEY)
|
|
153
158
|
if (!ref) {
|
|
154
159
|
throw new Error(`unreachable: HEAD is not present`)
|
|
155
160
|
}
|
|
@@ -160,18 +165,18 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
160
165
|
return ref ? cleanRefValue(ref) : undefined
|
|
161
166
|
}
|
|
162
167
|
this.branch = () => {
|
|
163
|
-
const currentHeadRef = refs.get(
|
|
168
|
+
const currentHeadRef = refs.get(REFS_HEAD_KEY)
|
|
164
169
|
if (!currentHeadRef) {
|
|
165
170
|
throw new Error('unreachable: ref HEAD not available')
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
if (currentHeadRef.value.includes(
|
|
173
|
+
if (currentHeadRef.value.includes(headValueRefPrefix)) {
|
|
169
174
|
const refName = cleanRefValue(currentHeadRef.value)
|
|
170
175
|
if (refs.has(refName))
|
|
171
176
|
return getLastItem(refName)
|
|
172
177
|
}
|
|
173
178
|
|
|
174
|
-
return
|
|
179
|
+
return REFS_HEAD_KEY // detached state
|
|
175
180
|
}
|
|
176
181
|
// endregion
|
|
177
182
|
|
|
@@ -226,7 +231,7 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
226
231
|
|
|
227
232
|
// region Commit lookups
|
|
228
233
|
const commitAtHead = () => {
|
|
229
|
-
return
|
|
234
|
+
return commitAtRefIn(REFS_HEAD_KEY, refs, commits)
|
|
230
235
|
}
|
|
231
236
|
const mustCommitAtHead = () => {
|
|
232
237
|
const commitHead = commitAtHead()
|
|
@@ -254,13 +259,14 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
254
259
|
|
|
255
260
|
const timestamp = new Date()
|
|
256
261
|
const changes = [...changesSinceLastCommit]
|
|
257
|
-
const sha = await
|
|
262
|
+
const sha = await calculateCommitHash({
|
|
258
263
|
message,
|
|
259
264
|
author,
|
|
260
265
|
changes,
|
|
261
266
|
parentRef: parent?.hash,
|
|
262
267
|
timestamp
|
|
263
268
|
})
|
|
269
|
+
const treeHash = await digest(obj)
|
|
264
270
|
|
|
265
271
|
const commit = {
|
|
266
272
|
hash: sha,
|
|
@@ -269,7 +275,8 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
269
275
|
changes: changes,
|
|
270
276
|
parent: parent?.hash,
|
|
271
277
|
timestamp,
|
|
272
|
-
to: version
|
|
278
|
+
to: version,
|
|
279
|
+
tree: treeHash
|
|
273
280
|
}
|
|
274
281
|
if (amend) {
|
|
275
282
|
const idx = commits.findIndex(c => c === parent)
|
|
@@ -283,7 +290,7 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
283
290
|
moveRef(headRef, commit)
|
|
284
291
|
} else {
|
|
285
292
|
// move detached HEAD to new commit
|
|
286
|
-
moveRef(
|
|
293
|
+
moveRef(REFS_HEAD_KEY, commit)
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
return sha
|
|
@@ -298,11 +305,11 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
298
305
|
if (commit) {
|
|
299
306
|
moveRef(branchRef, commit)
|
|
300
307
|
}
|
|
301
|
-
moveRef(
|
|
308
|
+
moveRef(REFS_HEAD_KEY, branchRef)
|
|
302
309
|
} else {
|
|
303
310
|
const [commit, isRef, refKey] = shaishToCommit(shaish, refs, commits)
|
|
304
311
|
rebuildChangeLog(commit)
|
|
305
|
-
moveRef(
|
|
312
|
+
moveRef(REFS_HEAD_KEY, isRef && refKey !== undefined ? refKey : commit)
|
|
306
313
|
}
|
|
307
314
|
}
|
|
308
315
|
this.createBranch = (name) => {
|
|
@@ -321,7 +328,7 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
321
328
|
}
|
|
322
329
|
const moveRef = (refName: string, value: string | Commit) => {
|
|
323
330
|
let ref = refs.get(refName)
|
|
324
|
-
const val = typeof value === 'string' ?
|
|
331
|
+
const val = typeof value === 'string' ? createHeadRefValue(value) : value.hash
|
|
325
332
|
if (!ref) {
|
|
326
333
|
ref = { name: getLastItem(refName), value: val }
|
|
327
334
|
} else {
|
|
@@ -337,7 +344,7 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
337
344
|
// https://github.com/isomorphic-git/isomorphic-git/blob/a623133345a5d8b6bb7a8352ea9702ce425d8266/src/utils/mergeTree.js#L33
|
|
338
345
|
|
|
339
346
|
if (typeof source !== 'string') {
|
|
340
|
-
// const srcHead =
|
|
347
|
+
// const srcHead = commitAtRefIn(REFS_HEAD, src.refs, src.commits)
|
|
341
348
|
throw new Error(`fatal: source type (${source instanceof Repository ? 'Repository' : 'History'}) not implemented`)
|
|
342
349
|
}
|
|
343
350
|
|
|
@@ -392,15 +399,6 @@ export const Repository = function <T extends { [k: PropertyKey]: any }>(
|
|
|
392
399
|
} as any as RespositoryObjectType
|
|
393
400
|
|
|
394
401
|
const getLastItem = (thePath: string) => thePath.substring(thePath.lastIndexOf('/') + 1)
|
|
395
|
-
const cleanRefValue = (ref: string) => ref.replace(refPrefix, '')
|
|
396
|
-
const brancheNameToRef = (name: string) => {
|
|
397
|
-
return `refs/heads/${name}`
|
|
398
|
-
}
|
|
399
|
-
const validateBranchName = (name: string) => {
|
|
400
|
-
if (!validBranch(name)) {
|
|
401
|
-
throw new Error(`invalid ref name`)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
402
|
|
|
405
403
|
/**
|
|
406
404
|
* Traverses the commit tree backwards and reassembles the changelog
|
|
@@ -434,13 +432,13 @@ const mapPath = (from: Commit, to: Commit, commits: Commit[]): [isAncestor: bool
|
|
|
434
432
|
* @param references
|
|
435
433
|
* @param commitsList
|
|
436
434
|
*/
|
|
437
|
-
const
|
|
435
|
+
const commitAtRefIn = (ref: string, references: Map<string, Reference>, commitsList: Commit[]) => {
|
|
438
436
|
const reference = references.get(ref)
|
|
439
437
|
if (!reference) {
|
|
440
438
|
throw new Error(`unreachable: '${ref}' is not present`)
|
|
441
439
|
}
|
|
442
440
|
let commitHash
|
|
443
|
-
if (reference.value.includes(
|
|
441
|
+
if (reference.value.includes(headValueRefPrefix)) {
|
|
444
442
|
const refKey = cleanRefValue(reference.value)
|
|
445
443
|
const targetRef = references.get(refKey)
|
|
446
444
|
if (!targetRef) {
|
|
@@ -477,9 +475,9 @@ const shaishToCommit = (shaish: string, references: Map<string, Reference>, comm
|
|
|
477
475
|
isRef = true
|
|
478
476
|
refKey = name
|
|
479
477
|
sha = ref.value
|
|
480
|
-
if (sha.includes(
|
|
478
|
+
if (sha.includes(headValueRefPrefix)) {
|
|
481
479
|
const cleanedRef = cleanRefValue(sha)
|
|
482
|
-
const c =
|
|
480
|
+
const c = commitAtRefIn(cleanedRef, references, commitsList)
|
|
483
481
|
if (!c) {
|
|
484
482
|
throw new Error(`${cleanedRef} points to non-existing commit`)
|
|
485
483
|
}
|
|
@@ -500,6 +498,24 @@ const shaishToCommit = (shaish: string, references: Map<string, Reference>, comm
|
|
|
500
498
|
return [found[0], isRef, refKey]
|
|
501
499
|
}
|
|
502
500
|
|
|
501
|
+
export const createHeadRefValue = (refKey: string) => {
|
|
502
|
+
return `${headValueRefPrefix}${refKey}`
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export const isTagRef = (refKey: string) => refKey.indexOf(tagRefPathPrefix) > -1
|
|
506
|
+
|
|
507
|
+
export const cleanRefValue = (ref: string) => ref.replace(headValueRefPrefix, '')
|
|
508
|
+
|
|
509
|
+
export const brancheNameToRef = (name: string) => {
|
|
510
|
+
return `${headsRefPathPrefix}${name}`
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const validateBranchName = (name: string) => {
|
|
514
|
+
if (!validBranch(name)) {
|
|
515
|
+
throw new Error(`invalid ref name`)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
503
519
|
/**
|
|
504
520
|
* Prints the underlying changelog of a repository
|
|
505
521
|
* @param repository
|
|
@@ -508,7 +524,7 @@ export const printChangeLog = <T>(repository: RepositoryObject<T>) => {
|
|
|
508
524
|
console.log('----------------------------------------------------------')
|
|
509
525
|
console.log(`Changelog at ${repository.head()}`)
|
|
510
526
|
const history = repository.getHistory()
|
|
511
|
-
const head =
|
|
527
|
+
const head = commitAtRefIn(repository.head(), history.refs, history.commits)
|
|
512
528
|
if (!head) {
|
|
513
529
|
throw new Error(`fatal: HEAD is not defined`)
|
|
514
530
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import {cleanAuthor} from './utils'
|
|
3
|
+
|
|
4
|
+
test('author <email@domain.info>', t => {
|
|
5
|
+
const [name, email] = cleanAuthor('author <email@domain.info>')
|
|
6
|
+
t.is(name, 'author')
|
|
7
|
+
t.is(email, 'email@domain.info')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('author with space <email@domain.info>', t => {
|
|
11
|
+
const [name, email] = cleanAuthor('author with space <email@domain.info>')
|
|
12
|
+
t.is(name, 'author with space')
|
|
13
|
+
t.is(email, 'email@domain.info')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('author @handle', t => {
|
|
17
|
+
const [name, email] = cleanAuthor('author @handle')
|
|
18
|
+
t.is(name, 'author')
|
|
19
|
+
t.is(email, '@handle')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('author with space @handle', t => {
|
|
23
|
+
const [name, email] = cleanAuthor('author with space @handle')
|
|
24
|
+
t.is(name, 'author with space')
|
|
25
|
+
t.is(email, '@handle')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('email@domain.info', t => {
|
|
29
|
+
const [name, email] = cleanAuthor('email@domain.info')
|
|
30
|
+
t.is(name, '')
|
|
31
|
+
t.is(email, 'email@domain.info')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('@handle', t => {
|
|
35
|
+
const [name, email] = cleanAuthor('@handle')
|
|
36
|
+
t.is(name, '@handle')
|
|
37
|
+
t.is(email, '')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('empty author', t => {
|
|
41
|
+
t.throws(() => {
|
|
42
|
+
cleanAuthor('')
|
|
43
|
+
}, {message: 'author not provided'})
|
|
44
|
+
})
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// [RFC5322](https://www.ietf.org/rfc/rfc5322.txt)
|
|
2
|
+
const emailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
|
|
3
|
+
|
|
4
|
+
export const cleanAuthor = (author: string): [name: string, email: string] => {
|
|
5
|
+
if (author === '') {
|
|
6
|
+
throw new Error(`author not provided`)
|
|
7
|
+
}
|
|
8
|
+
// author name <email>
|
|
9
|
+
let strings = author.split(' <')
|
|
10
|
+
if (strings.length > 1) {
|
|
11
|
+
return [strings[0], strings[1].replace('>', '')]
|
|
12
|
+
}
|
|
13
|
+
// author name @handle
|
|
14
|
+
strings = author.split(' @')
|
|
15
|
+
if (strings.length > 1) {
|
|
16
|
+
return [strings[0], `@${strings[1]}`]
|
|
17
|
+
}
|
|
18
|
+
// email@domain.com
|
|
19
|
+
if (emailRegex.test(author)) {
|
|
20
|
+
return ['', author]
|
|
21
|
+
}
|
|
22
|
+
// unrecognized format
|
|
23
|
+
return [author, '']
|
|
24
|
+
}
|