@dotinc/ogre 0.1.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/.nyc_output/34733e82-2b34-4674-aa72-94cab1c84e85.json +1 -0
- package/.nyc_output/3fb03278-d8e2-4673-abde-3cce50eb4143.json +1 -0
- package/.nyc_output/438de35e-20e6-470d-bfdb-d9b5fbc62f85.json +1 -0
- package/.nyc_output/61ba5903-4249-44c6-afe4-60e536bb69e0.json +1 -0
- package/.nyc_output/67c0b0a0-d3e0-4e7d-85e9-140cf10bf6e9.json +1 -0
- package/.nyc_output/77ec015f-70ab-47be-9757-17b5a39f4100.json +1 -0
- package/.nyc_output/processinfo/34733e82-2b34-4674-aa72-94cab1c84e85.json +1 -0
- package/.nyc_output/processinfo/3fb03278-d8e2-4673-abde-3cce50eb4143.json +1 -0
- package/.nyc_output/processinfo/438de35e-20e6-470d-bfdb-d9b5fbc62f85.json +1 -0
- package/.nyc_output/processinfo/61ba5903-4249-44c6-afe4-60e536bb69e0.json +1 -0
- package/.nyc_output/processinfo/67c0b0a0-d3e0-4e7d-85e9-140cf10bf6e9.json +1 -0
- package/.nyc_output/processinfo/77ec015f-70ab-47be-9757-17b5a39f4100.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +22 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/commit.ts.html +220 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/hash.ts.html +331 -0
- package/coverage/lcov-report/index.html +176 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/ref.ts.html +115 -0
- package/coverage/lcov-report/repository.ts.html +1648 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/test.utils.ts.html +223 -0
- package/coverage/lcov.info +660 -0
- package/lib/commit.d.ts +18 -0
- package/lib/commit.js +8 -0
- package/lib/hash.d.ts +23 -0
- package/lib/hash.js +82 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +7 -0
- package/lib/interfaces.d.ts +14 -0
- package/lib/interfaces.js +2 -0
- package/lib/ref.d.ts +2 -0
- package/lib/ref.js +13 -0
- package/lib/repository.d.ts +29 -0
- package/lib/repository.js +455 -0
- package/lib/size.d.ts +4 -0
- package/lib/size.js +31 -0
- package/lib/test.utils.d.ts +17 -0
- package/lib/test.utils.js +41 -0
- package/package.json +49 -0
- package/src/branch.test.ts +58 -0
- package/src/checkout.test.ts +101 -0
- package/src/commit.test.ts +168 -0
- package/src/commit.ts +45 -0
- package/src/hash.ts +82 -0
- package/src/index.ts +5 -0
- package/src/interfaces.ts +19 -0
- package/src/merge.test.ts +38 -0
- package/src/ref.ts +10 -0
- package/src/repository.test.ts +26 -0
- package/src/repository.ts +521 -0
- package/src/size.ts +26 -0
- package/src/test.utils.ts +46 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sumChanges = exports.addOneStep = exports.updateHeaderData = exports.getBaseline = exports.testAuthor = exports.ComplexObject = exports.NestedObject = void 0;
|
|
4
|
+
const uuid_1 = require("uuid");
|
|
5
|
+
const repository_1 = require("./repository");
|
|
6
|
+
class NestedObject {
|
|
7
|
+
}
|
|
8
|
+
exports.NestedObject = NestedObject;
|
|
9
|
+
class ComplexObject {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.nested = [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.ComplexObject = ComplexObject;
|
|
15
|
+
exports.testAuthor = 'User name <name@domain.com>';
|
|
16
|
+
async function getBaseline() {
|
|
17
|
+
const co = new ComplexObject();
|
|
18
|
+
const repo = new repository_1.Repository(co, {});
|
|
19
|
+
return [repo, repo.data];
|
|
20
|
+
}
|
|
21
|
+
exports.getBaseline = getBaseline;
|
|
22
|
+
function updateHeaderData(wrapped) {
|
|
23
|
+
wrapped.uuid = (0, uuid_1.v4)();
|
|
24
|
+
wrapped.name = 'my first process template';
|
|
25
|
+
wrapped.description = 'now we have a description';
|
|
26
|
+
return 3; // change entries
|
|
27
|
+
}
|
|
28
|
+
exports.updateHeaderData = updateHeaderData;
|
|
29
|
+
function addOneStep(wrapped) {
|
|
30
|
+
const pe = new NestedObject();
|
|
31
|
+
pe.uuid = (0, uuid_1.v4)();
|
|
32
|
+
pe.name = 'first name';
|
|
33
|
+
wrapped.nested.push(pe);
|
|
34
|
+
wrapped.nested[0].name = 'new name';
|
|
35
|
+
return 3; // change entries
|
|
36
|
+
}
|
|
37
|
+
exports.addOneStep = addOneStep;
|
|
38
|
+
function sumChanges(commits) {
|
|
39
|
+
return commits === null || commits === void 0 ? void 0 : commits.map(c => c.changes.length).reduce((p, c) => p + c, 0);
|
|
40
|
+
}
|
|
41
|
+
exports.sumChanges = sumChanges;
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dotinc/ogre",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "https://github.com/dotindustries/ogre.git"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"description": "Git-like repository for in-memory object versioning",
|
|
9
|
+
"main": "lib/index.ts",
|
|
10
|
+
"types": "lib/index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -p tsconfig.build.json",
|
|
13
|
+
"test": "nyc --reporter=lcov ava",
|
|
14
|
+
"test.watch": "ava --watch",
|
|
15
|
+
"coverage:html": "nyc report --reporter=html"
|
|
16
|
+
},
|
|
17
|
+
"ava": {
|
|
18
|
+
"timeout": "60s",
|
|
19
|
+
"files": [
|
|
20
|
+
"./src/**/*.test.ts"
|
|
21
|
+
],
|
|
22
|
+
"extensions": [
|
|
23
|
+
"ts"
|
|
24
|
+
],
|
|
25
|
+
"require": [
|
|
26
|
+
"ts-node/register"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"author": "János Veres @nadilas",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@ava/typescript": "^3.0.1",
|
|
33
|
+
"@types/node": "^17.0.21",
|
|
34
|
+
"@types/uuid": "^8.3.4",
|
|
35
|
+
"ava": "^4.0.1",
|
|
36
|
+
"coveralls": "^3.1.1",
|
|
37
|
+
"nyc": "^15.1.0",
|
|
38
|
+
"ts-node": "^10.7.0",
|
|
39
|
+
"tslib": "^2.3.1",
|
|
40
|
+
"turbo": "^1.1.5",
|
|
41
|
+
"typescript": "^4.5.5",
|
|
42
|
+
"uuid": "^8.3.2"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"registry": "https://registry.npmjs.org/",
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"gitHead": "d1ea04300335cd7618f4cf66251123f489d17b1c"
|
|
49
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import {getBaseline, sumChanges, testAuthor} from './test.utils'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
test('current branch on empty repo is HEAD', async t => {
|
|
6
|
+
const [repo] = await getBaseline()
|
|
7
|
+
|
|
8
|
+
t.is(repo.branch(), 'HEAD', 'invalid current branch')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('first commit goes onto default \'main\' branch', async t => {
|
|
12
|
+
const [repo] = await getBaseline()
|
|
13
|
+
repo.data.name = 'new name'
|
|
14
|
+
await repo.commit('initial commit', testAuthor)
|
|
15
|
+
|
|
16
|
+
t.is(repo.branch(), 'main', 'invalid current branch')
|
|
17
|
+
t.is(repo.head(), 'refs/heads/main', 'invalid HEAD')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('fails to create a branch with empty repo', async t => {
|
|
21
|
+
const [ repo ] = await getBaseline()
|
|
22
|
+
let history = repo.getHistory()
|
|
23
|
+
t.is(history.commits.length, 0, 'incorrect # of commits')
|
|
24
|
+
t.is(sumChanges(history?.commits), 0, 'new branch w/ incorrect # of changelog entries')
|
|
25
|
+
|
|
26
|
+
t.throws(() => {
|
|
27
|
+
// cannot point new branch to nothing on empty repo
|
|
28
|
+
repo.createBranch('new_feature')
|
|
29
|
+
}, {message: 'fatal: not a valid object name: \'main\''})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('checkout new branch with empty repo', async t => {
|
|
33
|
+
const [ repo ] = await getBaseline()
|
|
34
|
+
|
|
35
|
+
repo.checkout('new_feature', true)
|
|
36
|
+
t.is(repo.head(), 'refs/heads/new_feature', 'HEAD did not move to new branch')
|
|
37
|
+
t.is(repo.ref('/refs/heads/main'), undefined, 'main should not be pointing to anything')
|
|
38
|
+
t.is(repo.ref('refs/heads/new_feature'), undefined, 'new_feature should not be pointing to anything')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('creating a valid branch on a baseline', async t => {
|
|
42
|
+
const [ repo ] = await getBaseline()
|
|
43
|
+
repo.data.name = 'new name'
|
|
44
|
+
const commit = await repo.commit('simple change', testAuthor)
|
|
45
|
+
const ref = repo.createBranch('new_feature')
|
|
46
|
+
t.is(ref, 'refs/heads/new_feature', 'invalid branch ref created')
|
|
47
|
+
t.is(repo.ref(ref), commit, 'new branch is pointing to wrong commit')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('cannot create new branch with invalid name', async t => {
|
|
51
|
+
const [ repo ] = await getBaseline()
|
|
52
|
+
|
|
53
|
+
for (const name of ['', '-foo', 'HEAD']) {
|
|
54
|
+
t.throws(() => {
|
|
55
|
+
repo.createBranch(name)
|
|
56
|
+
}, {message: 'invalid ref name'})
|
|
57
|
+
}
|
|
58
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import {addOneStep, ComplexObject, getBaseline, sumChanges, testAuthor, updateHeaderData} from './test.utils'
|
|
3
|
+
import {Repository} from './repository'
|
|
4
|
+
|
|
5
|
+
test('checkout prev commit', async t => {
|
|
6
|
+
const [ repo, wrapped ] = await getBaseline()
|
|
7
|
+
|
|
8
|
+
updateHeaderData(wrapped)
|
|
9
|
+
const headerDataHash = await repo.commit('header data', testAuthor)
|
|
10
|
+
|
|
11
|
+
addOneStep(wrapped)
|
|
12
|
+
await repo.commit('first step', testAuthor)
|
|
13
|
+
|
|
14
|
+
repo.checkout(headerDataHash)
|
|
15
|
+
const head = repo.head()
|
|
16
|
+
const history = repo.getHistory()
|
|
17
|
+
t.is(sumChanges(history.commits), 3, `incorrect # of changelog entries`)
|
|
18
|
+
t.is(history.commits.length, 1, 'incorrect # of commits')
|
|
19
|
+
t.is(head, headerDataHash, `points to wrong commit`)
|
|
20
|
+
t.is(repo.branch(), 'HEAD', 'repo is not in detached state')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('checkout new branch with simple name', async t => {
|
|
24
|
+
const [ repo ] = await getBaseline()
|
|
25
|
+
repo.data.name = 'new name'
|
|
26
|
+
await repo.commit('simple change', testAuthor)
|
|
27
|
+
|
|
28
|
+
const ref = repo.createBranch('new_feature')
|
|
29
|
+
repo.checkout('new_feature')
|
|
30
|
+
t.is(repo.head(), ref, 'HEAD is not moved to target branch')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('checkout new branch with full ref name', async t => {
|
|
34
|
+
const [ repo ] = await getBaseline()
|
|
35
|
+
repo.data.name = 'new name'
|
|
36
|
+
await repo.commit('simple change', testAuthor)
|
|
37
|
+
|
|
38
|
+
const ref = repo.createBranch('new_feature')
|
|
39
|
+
repo.checkout(ref)
|
|
40
|
+
t.is(repo.head(), ref, 'HEAD is not moved to target branch')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('checkout commit which has two refs pointing leaves HEAD detached', async t => {
|
|
44
|
+
const [ repo ] = await getBaseline()
|
|
45
|
+
repo.data.name = 'new name'
|
|
46
|
+
const commit = await repo.commit('simple change', testAuthor)
|
|
47
|
+
|
|
48
|
+
repo.createBranch('new_feature')
|
|
49
|
+
repo.checkout(commit)
|
|
50
|
+
t.is(repo.ref('refs/heads/main'), commit, 'main does not point to commit')
|
|
51
|
+
t.is(repo.ref('refs/heads/new_feature'), commit, 'new_feature does not point to commit')
|
|
52
|
+
t.is(repo.branch(), 'HEAD', 'HEAD is not detached at commit')
|
|
53
|
+
t.is(repo.head(), commit, 'HEAD is not pointing to commit')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('checkout new branch moves head to new branch', async t => {
|
|
57
|
+
const [ repo ] = await getBaseline()
|
|
58
|
+
repo.data.name = 'new name'
|
|
59
|
+
await repo.commit('simple change', testAuthor)
|
|
60
|
+
|
|
61
|
+
const ref = repo.createBranch('new_feature')
|
|
62
|
+
repo.checkout('new_feature')
|
|
63
|
+
t.is(repo.head(), ref, 'HEAD is not moved to target branch')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('checkout and create new branch on empty main', async t => {
|
|
67
|
+
const [repo] = await getBaseline()
|
|
68
|
+
|
|
69
|
+
repo.checkout('new_feature', true)
|
|
70
|
+
t.is(repo.head(), 'refs/heads/new_feature', 'HEAD should point to empty branch')
|
|
71
|
+
t.is(repo.branch(), 'HEAD', 'branch still should be empty')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('checkout and create new branch with at least 1 commit', async t => {
|
|
75
|
+
const [repo] = await getBaseline()
|
|
76
|
+
repo.data.name = 'new name'
|
|
77
|
+
const commit = await repo.commit('simple change', testAuthor)
|
|
78
|
+
|
|
79
|
+
repo.checkout('new_feature', true)
|
|
80
|
+
t.is(repo.head(), 'refs/heads/new_feature', 'HEAD should point to new branch')
|
|
81
|
+
t.is(repo.ref('refs/heads/new_feature'), commit, 'branch is not pointing to last HEAD commit')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
test('replacing default branch on empty master removes main', async t => {
|
|
86
|
+
const repo = new Repository(new ComplexObject(), {})
|
|
87
|
+
|
|
88
|
+
// replacing default main branch by moving HEAD to new branch
|
|
89
|
+
// is OK even on empty repo
|
|
90
|
+
repo.checkout('new_feature', true)
|
|
91
|
+
const history = repo.getHistory()
|
|
92
|
+
t.is(sumChanges(history?.commits), 0, 'new branch w/ incorrect # of changelog entries')
|
|
93
|
+
|
|
94
|
+
repo.data.name = "name changed"
|
|
95
|
+
repo.data.description = "description changed"
|
|
96
|
+
await repo.commit('description changes', testAuthor)
|
|
97
|
+
|
|
98
|
+
t.throws(() => {
|
|
99
|
+
repo.checkout('main')
|
|
100
|
+
}, {message: `pathspec 'main' did not match any known refs`})
|
|
101
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import { addOneStep, getBaseline, sumChanges, testAuthor, updateHeaderData } from './test.utils'
|
|
3
|
+
|
|
4
|
+
test('baseline with 1 commit and zero changelog entries', async t => {
|
|
5
|
+
const [repo] = await getBaseline()
|
|
6
|
+
|
|
7
|
+
const history = repo.getHistory()
|
|
8
|
+
t.is(sumChanges(history.commits), 0, 'has changelog entries')
|
|
9
|
+
t.is(history.commits.length, 0, 'incorrect # of commits')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('head points to main', async t => {
|
|
13
|
+
const [repo] = await getBaseline()
|
|
14
|
+
|
|
15
|
+
t.is(repo.head(), 'refs/heads/main', 'head not pointing where it should')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('no commit without changes', async t => {
|
|
19
|
+
const [repo] = await getBaseline()
|
|
20
|
+
|
|
21
|
+
await t.throwsAsync(async () => {
|
|
22
|
+
return await repo.commit('baseline', testAuthor)
|
|
23
|
+
}, { message: 'no changes to commit' })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('no commit without changes after recent commit', async t => {
|
|
27
|
+
const [repo] = await getBaseline()
|
|
28
|
+
repo.data.name = 'new name'
|
|
29
|
+
await repo.commit('baseline', testAuthor)
|
|
30
|
+
|
|
31
|
+
await t.throwsAsync(async () => {
|
|
32
|
+
return await repo.commit('baseline', testAuthor)
|
|
33
|
+
}, { message: 'no changes to commit' })
|
|
34
|
+
})
|
|
35
|
+
test('no commit --amend without commit', async t => {
|
|
36
|
+
const [repo] = await getBaseline()
|
|
37
|
+
repo.data.name = 'new name'
|
|
38
|
+
|
|
39
|
+
await t.throwsAsync(async () => {
|
|
40
|
+
return await repo.commit('baseline', testAuthor, true)
|
|
41
|
+
}, { message: 'no commit to amend' })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('main moves to recent commit', async t => {
|
|
45
|
+
const [repo] = await getBaseline()
|
|
46
|
+
repo.data.name = 'new name'
|
|
47
|
+
const hash = await repo.commit('baseline', testAuthor)
|
|
48
|
+
|
|
49
|
+
t.is(repo.ref('refs/heads/main'), hash, 'head does not point to recent commit')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('two commits with 3 changes', async t => {
|
|
53
|
+
const [repo, wrapped] = await getBaseline()
|
|
54
|
+
updateHeaderData(wrapped)
|
|
55
|
+
await repo.commit('header data', testAuthor)
|
|
56
|
+
|
|
57
|
+
const history = repo.getHistory()
|
|
58
|
+
t.is(sumChanges(history.commits), 3, 'incorrect # of changelog entries')
|
|
59
|
+
t.is(history.commits.length, 1, 'incorrect # of commits')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('array push double-change, 6 changes, 3 commits', async t => {
|
|
63
|
+
const [repo, wrapped] = await getBaseline()
|
|
64
|
+
|
|
65
|
+
updateHeaderData(wrapped)
|
|
66
|
+
await repo.commit('header data', testAuthor)
|
|
67
|
+
|
|
68
|
+
addOneStep(wrapped)
|
|
69
|
+
await repo.commit('first step', testAuthor)
|
|
70
|
+
|
|
71
|
+
const history = repo.getHistory()
|
|
72
|
+
t.is(sumChanges(history.commits), 6, 'incorrect # of changelog entries')
|
|
73
|
+
t.is(history.commits.length, 2, 'incorrect # of commits')
|
|
74
|
+
t.is(history.commits[0].changes.length, 3, '#incorrect # of changes in commit#1')
|
|
75
|
+
t.is(history.commits[1].changes.length, 3, '#incorrect # of changes in commit#2')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('all refs OK, when committing on new branch while main is empty main', async t => {
|
|
79
|
+
const [repo] = await getBaseline()
|
|
80
|
+
repo.checkout('new_feature', true)
|
|
81
|
+
repo.data.name = 'new name'
|
|
82
|
+
const commit = await repo.commit('simple change', testAuthor)
|
|
83
|
+
|
|
84
|
+
t.is(repo.ref('refs/heads/main'), undefined, 'main should not point to a commit')
|
|
85
|
+
t.is(repo.ref('refs/heads/new_feature'), commit, 'new_feature should point to last commit')
|
|
86
|
+
t.is(repo.branch(), 'new_feature', 'branch should now be visible')
|
|
87
|
+
t.is(repo.head(), 'refs/heads/new_feature', 'HEAD is pointing to wrong branch')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('commit --amend changes hash on content change', async t => {
|
|
91
|
+
const [repo] = await getBaseline()
|
|
92
|
+
repo.data.name = 'new name'
|
|
93
|
+
const commitToAmend = await repo.commit('name change', testAuthor)
|
|
94
|
+
repo.data.description = 'new description'
|
|
95
|
+
const changedHash = await repo.commit('name and description change', testAuthor, true)
|
|
96
|
+
t.not(changedHash, commitToAmend, 'hash should have changed')
|
|
97
|
+
const history = repo.getHistory()
|
|
98
|
+
t.is(history.commits.length, 1, 'wrong # of commits')
|
|
99
|
+
t.is(sumChanges(history.commits), 2, 'wrong # of changes')
|
|
100
|
+
t.is(repo.head(), 'refs/heads/main', 'HEAD is not pointing to main')
|
|
101
|
+
t.is(repo.branch(), 'main', 'we are on the wrong branch')
|
|
102
|
+
t.is(repo.ref('refs/heads/main'), changedHash, 'main should point to changed commit hash')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('commit --amend changes hash on message change', async t => {
|
|
106
|
+
const [repo] = await getBaseline()
|
|
107
|
+
repo.data.name = 'new name'
|
|
108
|
+
const commitToAmend = await repo.commit('name change', testAuthor)
|
|
109
|
+
const changedHash = await repo.commit('initial setup', testAuthor, true)
|
|
110
|
+
t.not(changedHash, commitToAmend, 'hash should have changed')
|
|
111
|
+
const history = repo.getHistory()
|
|
112
|
+
t.is(history.commits.length, 1, 'wrong # of commits')
|
|
113
|
+
t.is(sumChanges(history.commits), 1, 'wrong # of changes')
|
|
114
|
+
t.is(repo.head(), 'refs/heads/main', 'HEAD is not pointing to main')
|
|
115
|
+
t.is(repo.branch(), 'main', 'we are on the wrong branch')
|
|
116
|
+
t.is(repo.ref('refs/heads/main'), changedHash, 'main should point to changed commit hash')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('commit at detached HEAD does not affect main, but moves head', async t => {
|
|
120
|
+
const [repo] = await getBaseline()
|
|
121
|
+
repo.data.name = 'new name'
|
|
122
|
+
const v1 = repo.data
|
|
123
|
+
const commit = await repo.commit('name change', testAuthor)
|
|
124
|
+
repo.data.description = 'new fancy description'
|
|
125
|
+
const last = await repo.commit('desc change', testAuthor)
|
|
126
|
+
repo.checkout(commit)
|
|
127
|
+
t.is(repo.head(), commit, 'HEAD did not move to commit')
|
|
128
|
+
t.is(repo.branch(), 'HEAD', 'repo is not in detached state')
|
|
129
|
+
t.deepEqual(v1, repo.data, 'object state does not match')
|
|
130
|
+
|
|
131
|
+
repo.data.description = 'a different description'
|
|
132
|
+
const commitOnDetached = await repo.commit('msg', testAuthor)
|
|
133
|
+
t.is(repo.head(), commitOnDetached, 'HEAD did not move to commit')
|
|
134
|
+
t.is(repo.ref('refs/heads/main'), last, 'main branch did not stay at last commit')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('commit at detached HEAD saved to a branch', async t => {
|
|
138
|
+
const [repo] = await getBaseline()
|
|
139
|
+
repo.data.name = 'new name'
|
|
140
|
+
const commit = await repo.commit('name change', testAuthor)
|
|
141
|
+
repo.data.description = 'new fancy description'
|
|
142
|
+
await repo.commit('desc change', testAuthor)
|
|
143
|
+
repo.checkout(commit)
|
|
144
|
+
|
|
145
|
+
repo.data.description = 'a different description'
|
|
146
|
+
const commitOnDetached = await repo.commit('msg', testAuthor)
|
|
147
|
+
|
|
148
|
+
const savepointRef = repo.createBranch('savepoint')
|
|
149
|
+
t.is(repo.ref(savepointRef), commitOnDetached, 'savepoint branch should point to last detached commit')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
test('commit --amend changes hash on message change even in detached HEAD', async t => {
|
|
154
|
+
const [repo] = await getBaseline()
|
|
155
|
+
repo.data.name = 'new name'
|
|
156
|
+
const commitToAmend = await repo.commit('name change', testAuthor)
|
|
157
|
+
repo.data.description = 'desc change'
|
|
158
|
+
const descCommit = await repo.commit('desc change', testAuthor)
|
|
159
|
+
repo.checkout(commitToAmend)
|
|
160
|
+
const changedHash = await repo.commit('initial setup', testAuthor, true)
|
|
161
|
+
t.not(changedHash, commitToAmend, 'hash should have changed')
|
|
162
|
+
const history = repo.getHistory()
|
|
163
|
+
t.is(history.commits.length, 1, 'wrong # of commits')
|
|
164
|
+
t.is(sumChanges(history.commits), 1, 'wrong # of changes')
|
|
165
|
+
t.is(repo.branch(), 'HEAD', 'not in detached state')
|
|
166
|
+
t.is(repo.head(), changedHash, 'HEAD is not pointing to detached commit')
|
|
167
|
+
t.is(repo.ref('refs/heads/main'), descCommit, 'main should point to changed commit hash')
|
|
168
|
+
})
|
package/src/commit.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Change } from './interfaces'
|
|
2
|
+
import { digest } from './hash'
|
|
3
|
+
|
|
4
|
+
export interface Commit {
|
|
5
|
+
// The hash of the commit
|
|
6
|
+
// Is an sha256 of:
|
|
7
|
+
// - tree object reference (changes?)
|
|
8
|
+
// - parent object reference (parent hash)
|
|
9
|
+
// - author
|
|
10
|
+
// - author commit timestamp with timezone
|
|
11
|
+
// - commit message
|
|
12
|
+
hash: string
|
|
13
|
+
|
|
14
|
+
message: string | undefined
|
|
15
|
+
author: string
|
|
16
|
+
|
|
17
|
+
// The hash of the parent commit
|
|
18
|
+
parent: string | undefined
|
|
19
|
+
|
|
20
|
+
// The diff of this commit from the parent
|
|
21
|
+
changes: Change[]
|
|
22
|
+
|
|
23
|
+
// Commit timestamp with timezone
|
|
24
|
+
timestamp: Date
|
|
25
|
+
|
|
26
|
+
// The version number to the corresponding changelog entry (not zero-based index)
|
|
27
|
+
// Therefore it can be used as an index, when accessing the changelog to
|
|
28
|
+
// retrieve the relevant changes e.g.:
|
|
29
|
+
// ```
|
|
30
|
+
// const changes = changeLog.slice(commit.from, commit.to)
|
|
31
|
+
// ```
|
|
32
|
+
to: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface HashContent {
|
|
36
|
+
message: string
|
|
37
|
+
author: string
|
|
38
|
+
parentRef: string | undefined
|
|
39
|
+
changes: Change[]
|
|
40
|
+
timestamp: Date
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function calculateHash(content: HashContent) {
|
|
44
|
+
return digest(content)
|
|
45
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit goes to https://github.com/juanelas/object-sha
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This module runs perfectly in node.js and browsers
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns a string with a hexadecimal representation of the digest of the input object using a given hash algorithm.
|
|
12
|
+
* It first creates an array of the object values ordered by the object keys (using hashable(obj));
|
|
13
|
+
* then, it JSON.stringify-es it; and finally it hashes it.
|
|
14
|
+
*
|
|
15
|
+
* @param obj - An Object
|
|
16
|
+
* @param algorithm - For compatibility with browsers it should be 'SHA-1', 'SHA-256', 'SHA-384' and 'SHA-512'.
|
|
17
|
+
*
|
|
18
|
+
* @param isBrowser
|
|
19
|
+
* @throws {RangeError}
|
|
20
|
+
* Thrown if an invalid hash algorithm is selected.
|
|
21
|
+
*
|
|
22
|
+
* @returns a promise that resolves to a string with hexadecimal content.
|
|
23
|
+
*/
|
|
24
|
+
export function digest (obj: any, algorithm = 'SHA-256', isBrowser = false): Promise<string> { // eslint-disable-line
|
|
25
|
+
const algorithms = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']
|
|
26
|
+
if (!algorithms.includes(algorithm)) {
|
|
27
|
+
throw RangeError(`Valid hash algorithm values are any of ${JSON.stringify(algorithms)}`)
|
|
28
|
+
}
|
|
29
|
+
return (async function (obj, algorithm) {
|
|
30
|
+
const encoder = new TextEncoder()
|
|
31
|
+
const hashInput = encoder.encode(hashable(obj)).buffer
|
|
32
|
+
let digest = ''
|
|
33
|
+
|
|
34
|
+
if (isBrowser) {
|
|
35
|
+
const buf = await crypto.subtle.digest(algorithm, hashInput)
|
|
36
|
+
const h = '0123456789abcdef';
|
|
37
|
+
(new Uint8Array(buf)).forEach((v) => {
|
|
38
|
+
digest += h[v >> 4] + h[v & 15]
|
|
39
|
+
})
|
|
40
|
+
} else {
|
|
41
|
+
const nodeAlg = algorithm.toLowerCase().replace('-', '')
|
|
42
|
+
digest = require('crypto').createHash(nodeAlg).update(Buffer.from(hashInput)).digest('hex') // eslint-disable-line
|
|
43
|
+
}
|
|
44
|
+
/* eslint-enable no-lone-blocks */
|
|
45
|
+
return digest
|
|
46
|
+
})(obj, algorithm)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isObject (val: any): boolean {
|
|
50
|
+
return (val != null) && (typeof val === 'object') && !(Array.isArray(val))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function objectToArraySortedByKey (obj: any): any {
|
|
54
|
+
if (!isObject(obj) && !Array.isArray(obj)) {
|
|
55
|
+
return obj
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(obj)) {
|
|
58
|
+
return obj.map((item) => {
|
|
59
|
+
if (Array.isArray(item) || isObject(item)) {
|
|
60
|
+
return objectToArraySortedByKey(item)
|
|
61
|
+
}
|
|
62
|
+
return item
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
// if it is an object convert to array and sort
|
|
66
|
+
return Object.keys(obj) // eslint-disable-line
|
|
67
|
+
.sort()
|
|
68
|
+
.map((key) => {
|
|
69
|
+
return [key, objectToArraySortedByKey(obj[key])]
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* If the input object is not an Array, this function converts the object to an array, all the key-values to 2-arrays [key, value] and then sort the array by the keys. All the process is done recursively so objects inside objects or arrays are also ordered. Once the array is created the method returns the JSON.stringify() of the sorted array.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} obj the object
|
|
77
|
+
*
|
|
78
|
+
* @returns {string} a JSON stringify of the created sorted array
|
|
79
|
+
*/
|
|
80
|
+
const hashable = (obj: object) => {
|
|
81
|
+
return JSON.stringify(objectToArraySortedByKey(obj))
|
|
82
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Commit } from './commit'
|
|
2
|
+
|
|
3
|
+
export interface Reference {
|
|
4
|
+
name: string
|
|
5
|
+
// A reference can point to a commit via its sha256
|
|
6
|
+
// or it can point to a reference
|
|
7
|
+
value: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Change {
|
|
11
|
+
path: any[]
|
|
12
|
+
newValue: any | undefined
|
|
13
|
+
oldValue: any | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface History {
|
|
17
|
+
refs: Map<string, Reference>
|
|
18
|
+
commits: Commit[]
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import {getBaseline, testAuthor} from './test.utils'
|
|
3
|
+
|
|
4
|
+
test('merge with no commit fails', async t => {
|
|
5
|
+
const [repo] = await getBaseline()
|
|
6
|
+
repo.data.name = 'new name'
|
|
7
|
+
await repo.commit('simple change', testAuthor)
|
|
8
|
+
|
|
9
|
+
repo.createBranch('new_feature')
|
|
10
|
+
|
|
11
|
+
t.throws(() => {
|
|
12
|
+
repo.merge('new_feature')
|
|
13
|
+
}, { message: 'already up to date'})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('merge fast-forward', async t => {
|
|
17
|
+
const [repo] = await getBaseline()
|
|
18
|
+
repo.data.name = 'new name'
|
|
19
|
+
await repo.commit('simple change', testAuthor)
|
|
20
|
+
const masterCommitCount = repo.getHistory().commits.length
|
|
21
|
+
|
|
22
|
+
repo.checkout('new_branch', true)
|
|
23
|
+
repo.data.name = 'another name'
|
|
24
|
+
const minorHash = await repo.commit('minor change', testAuthor)
|
|
25
|
+
t.is(repo.head(), 'refs/heads/new_branch', 'HEAD not pointing to new_branch')
|
|
26
|
+
t.is(repo.ref('refs/heads/new_branch'), minorHash, 'branch did not move to new commit')
|
|
27
|
+
|
|
28
|
+
// go to destination branch
|
|
29
|
+
repo.checkout('main')
|
|
30
|
+
const mergeHash = repo.merge('new_branch')
|
|
31
|
+
const headRef = repo.head()
|
|
32
|
+
const refHash = repo.ref(headRef)
|
|
33
|
+
|
|
34
|
+
t.is(mergeHash, minorHash, 'did not fast-forward to expected commit')
|
|
35
|
+
t.is(refHash, mergeHash, `master is not at expected commit`)
|
|
36
|
+
t.is(repo.getHistory().commits.length, masterCommitCount+1, 'fast-forward failed, superfluous commit detected')
|
|
37
|
+
|
|
38
|
+
})
|
package/src/ref.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const bad = /(^|[/.])([/.]|$)|^@$|@{|[\x00-\x20\x7f~^:?*[\\]|\.lock(\/|$)/
|
|
2
|
+
const badBranch = /^(-|HEAD$)/
|
|
3
|
+
|
|
4
|
+
export function validRef (name: string, onelevel: boolean) {
|
|
5
|
+
return !bad.test(name) && (onelevel || name.includes('/'))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function validBranch (name: string) {
|
|
9
|
+
return validRef(name, true) && !badBranch.test(name)
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import { Repository } from './repository'
|
|
3
|
+
import {addOneStep, ComplexObject, getBaseline, sumChanges, testAuthor, updateHeaderData} from './test.utils'
|
|
4
|
+
|
|
5
|
+
test('reconstruction', async t => {
|
|
6
|
+
const [ repo, wrapped ] = await getBaseline()
|
|
7
|
+
|
|
8
|
+
let changeEntries = updateHeaderData(wrapped)
|
|
9
|
+
await repo.commit('header data', testAuthor)
|
|
10
|
+
|
|
11
|
+
changeEntries += addOneStep(wrapped)
|
|
12
|
+
const firstStep = await repo.commit('first step', testAuthor)
|
|
13
|
+
|
|
14
|
+
const history = repo.getHistory()
|
|
15
|
+
t.is(repo.head(), 'refs/heads/main', 'HEAD is wrong')
|
|
16
|
+
t.is(repo.ref('refs/heads/main'), firstStep, 'main is pointing at wrong commit')
|
|
17
|
+
t.is(history.commits.length, 2, 'incorrect # of commits')
|
|
18
|
+
|
|
19
|
+
// start reconstruction
|
|
20
|
+
const p = new ComplexObject()
|
|
21
|
+
const repo2 = new Repository(p, { history })
|
|
22
|
+
|
|
23
|
+
const history2 = repo2.getHistory()
|
|
24
|
+
t.is(history2.commits.length, 2, 'incorrect # of commits')
|
|
25
|
+
t.is(sumChanges(history2.commits), changeEntries, 'incorrect # of changelog entries')
|
|
26
|
+
})
|