Things
This commit is contained in:
parent
1a7400e38c
commit
4943d8790d
@ -13,6 +13,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.21",
|
"@types/chai": "^4.2.21",
|
||||||
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/node": "^16.4.13",
|
"@types/node": "^16.4.13",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
@ -22,6 +23,7 @@
|
|||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jssha": "^3.2.0"
|
"jssha": "^3.2.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,10 +2,12 @@ lockfileVersion: 5.3
|
|||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
'@types/chai': ^4.2.21
|
'@types/chai': ^4.2.21
|
||||||
|
'@types/lodash': ^4.14.178
|
||||||
'@types/mocha': ^9.0.0
|
'@types/mocha': ^9.0.0
|
||||||
'@types/node': ^16.4.13
|
'@types/node': ^16.4.13
|
||||||
chai: ^4.3.4
|
chai: ^4.3.4
|
||||||
jssha: ^3.2.0
|
jssha: ^3.2.0
|
||||||
|
lodash: ^4.17.21
|
||||||
mocha: ^9.0.3
|
mocha: ^9.0.3
|
||||||
nodemon: ^2.0.12
|
nodemon: ^2.0.12
|
||||||
ts-node: ^10.2.0
|
ts-node: ^10.2.0
|
||||||
@ -13,9 +15,11 @@ specifiers:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
jssha: 3.2.0
|
jssha: 3.2.0
|
||||||
|
lodash: 4.17.21
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/chai': 4.2.21
|
'@types/chai': 4.2.21
|
||||||
|
'@types/lodash': 4.14.178
|
||||||
'@types/mocha': 9.0.0
|
'@types/mocha': 9.0.0
|
||||||
'@types/node': 16.4.13
|
'@types/node': 16.4.13
|
||||||
chai: 4.3.4
|
chai: 4.3.4
|
||||||
@ -70,6 +74,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==}
|
resolution: {integrity: sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/lodash/4.14.178:
|
||||||
|
resolution: {integrity: sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/mocha/9.0.0:
|
/@types/mocha/9.0.0:
|
||||||
resolution: {integrity: sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==}
|
resolution: {integrity: sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -706,6 +714,10 @@ packages:
|
|||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash/4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/log-symbols/4.1.0:
|
/log-symbols/4.1.0:
|
||||||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
564
src/repo.ts
564
src/repo.ts
@ -11,15 +11,14 @@ export interface IDataStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type NodeType = "tree" | "blob";
|
export type NodeType = "tree" | "blob";
|
||||||
export type NodeHash = string;
|
export type ObjectID = string;
|
||||||
export type NodeFilename = string;
|
export type NodeFilename = string;
|
||||||
export type TreeEntry = [NodeType, NodeHash, NodeFilename];
|
export type TreeEntry = [NodeType, ObjectID, NodeFilename];
|
||||||
|
|
||||||
export type Commit = {
|
export type ICommit = {
|
||||||
id: string;
|
|
||||||
root: string;
|
root: string;
|
||||||
before: string;
|
before: string;
|
||||||
merge: string;
|
merge?: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,8 +48,57 @@ export const ObjectTypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ObjectTypeNames = keyof typeof ObjectTypes;
|
export type ObjectTypeNames = keyof typeof ObjectTypes;
|
||||||
|
export type TypedObjectData = {
|
||||||
|
type: ObjectTypeNames,
|
||||||
|
data: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
export default class Repository {
|
export default class Repository {
|
||||||
|
//#region static
|
||||||
|
|
||||||
|
static sha1(data: Uint8Array) {
|
||||||
|
const s = new SHA("SHA-1", "UINT8ARRAY");
|
||||||
|
s.update(data);
|
||||||
|
return s.getHash("HEX");
|
||||||
|
}
|
||||||
|
|
||||||
|
static sha256(data: Uint8Array) {
|
||||||
|
const s = new SHA("SHA3-256", "UINT8ARRAY");
|
||||||
|
s.update(data);
|
||||||
|
return s.getHash("HEX");
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateObjectID(data: Uint8Array) {
|
||||||
|
return this.sha1(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseObject(data: Uint8Array | undefined): TypedObjectData | undefined {
|
||||||
|
if (!data)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
let type = new TextDecoder().decode(data.slice(0, 4))
|
||||||
|
return {
|
||||||
|
type: type as ObjectTypeNames,
|
||||||
|
data: data.slice(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static buildObject(data: Uint8Array | string, type: ObjectTypeNames): ObjectTuple {
|
||||||
|
if (typeof data == "string") {
|
||||||
|
data = new TextEncoder().encode(data);
|
||||||
|
}
|
||||||
|
const objectID = this.generateObjectID(data);
|
||||||
|
|
||||||
|
let merged = new Uint8Array(5 + data.length);
|
||||||
|
merged.set(ObjectTypes[type], 0)
|
||||||
|
merged.set(data, 5);
|
||||||
|
|
||||||
|
return [objectID, merged]
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
//#region local variables
|
//#region local variables
|
||||||
#store: IDataStore;
|
#store: IDataStore;
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -61,17 +109,7 @@ export default class Repository {
|
|||||||
|
|
||||||
//#region private
|
//#region private
|
||||||
|
|
||||||
private sha1(data: Uint8Array) {
|
|
||||||
const s = new SHA("SHA-1", "UINT8ARRAY");
|
|
||||||
s.update(data);
|
|
||||||
return s.getHash("HEX");
|
|
||||||
}
|
|
||||||
|
|
||||||
private sha256(data: Uint8Array) {
|
|
||||||
const s = new SHA("SHA3-256", "UINT8ARRAY");
|
|
||||||
s.update(data);
|
|
||||||
return s.getHash("HEX");
|
|
||||||
}
|
|
||||||
|
|
||||||
private splitPath(path: string) {
|
private splitPath(path: string) {
|
||||||
const resolved = Path.resolve(path).slice(1);
|
const resolved = Path.resolve(path).slice(1);
|
||||||
@ -105,12 +143,6 @@ export default class Repository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readTree(id: string): Promise<TreeEntry[]> {
|
|
||||||
const tree = await this.readObject(id, true);
|
|
||||||
if (tree == undefined) throw new Error("Invalid treeID");
|
|
||||||
return this.parseTree(tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async makeCommit(treeID: string, old?: Commit, merge?: Commit) {
|
private async makeCommit(treeID: string, old?: Commit, merge?: Commit) {
|
||||||
if (!old) {
|
if (!old) {
|
||||||
// Could be called once more than necessary, if no HEAD exists.
|
// Could be called once more than necessary, if no HEAD exists.
|
||||||
@ -145,7 +177,7 @@ export default class Repository {
|
|||||||
if (typeof data == "string") {
|
if (typeof data == "string") {
|
||||||
data = new TextEncoder().encode(data);
|
data = new TextEncoder().encode(data);
|
||||||
}
|
}
|
||||||
const objectID = this.sha1(data);
|
const objectID = Repository.generateObjectID(data);
|
||||||
|
|
||||||
let merged = new Uint8Array(5 + data.length);
|
let merged = new Uint8Array(5 + data.length);
|
||||||
merged.set(ObjectTypes[type], 0)
|
merged.set(ObjectTypes[type], 0)
|
||||||
@ -165,15 +197,10 @@ export default class Repository {
|
|||||||
|
|
||||||
public async readObjectTyped(id: String) {
|
public async readObjectTyped(id: String) {
|
||||||
let data = await this.#store.get("objects/" + id);
|
let data = await this.#store.get("objects/" + id);
|
||||||
if (!data)
|
return Repository.parseObject(data);
|
||||||
return undefined;
|
}
|
||||||
|
|
||||||
|
|
||||||
let type = new TextDecoder().decode(data.slice(0, 4))
|
|
||||||
return {
|
|
||||||
type: type as ObjectTypeNames,
|
|
||||||
data: data.slice(5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async readObject(id: string, string: true): Promise<string | undefined>;
|
public async readObject(id: string, string: true): Promise<string | undefined>;
|
||||||
public async readObject(id: string, string?: false): Promise<Uint8Array | undefined>;
|
public async readObject(id: string, string?: false): Promise<Uint8Array | undefined>;
|
||||||
@ -204,51 +231,20 @@ export default class Repository {
|
|||||||
return type == "comm";
|
return type == "comm";
|
||||||
}
|
}
|
||||||
|
|
||||||
parseTree(treeStr: string | Uint8Array): TreeEntry[] {
|
private getVirtualRepo() {
|
||||||
if (typeof treeStr !== "string")
|
return new VirtualRepo(this);
|
||||||
treeStr = new TextDecoder().decode(treeStr);
|
|
||||||
return treeStr.split("\n").filter(e => e !== "").map((e) => {
|
|
||||||
const entry = e.split(" ") as TreeEntry;
|
|
||||||
const [type] = entry;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "blob":
|
|
||||||
case "tree":
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("Invalid tree type."); //Might be a newer version or so
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry;
|
async readTree(id: string): Promise<Tree> {
|
||||||
});
|
return Tree.load(this.getVirtualRepo(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseCommit(id: string, commitStr: string | Uint8Array,) {
|
async readBlob(id: string): Promise<Blob> {
|
||||||
if (typeof commitStr !== "string")
|
return Blob.load(this.getVirtualRepo(), id);
|
||||||
commitStr = new TextDecoder().decode(commitStr);
|
|
||||||
|
|
||||||
let commit: Commit = { id } as any;
|
|
||||||
for (const entry of commitStr.split("\n")) {
|
|
||||||
const [type] = entry.split(" ", 1);
|
|
||||||
const value = entry.slice(type.length + 1);
|
|
||||||
switch (type) {
|
|
||||||
case "tree": // TODO: Simple validity checks
|
|
||||||
commit.root = value;
|
|
||||||
break;
|
|
||||||
case "before": // TODO: Simple validity checks
|
|
||||||
commit.before = value;
|
|
||||||
break;
|
|
||||||
case "merge": // TODO: Simple validity checks
|
|
||||||
commit.merge = value;
|
|
||||||
break;
|
|
||||||
case "date":
|
|
||||||
commit.date = new Date(value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return commit;
|
|
||||||
}
|
|
||||||
|
|
||||||
async readCommit(id: string): Promise<Commit> {
|
async readCommit(id: string): Promise<Commit> {
|
||||||
const commitStr = await this.readObject(id, true);
|
const commitStr = await this.readObject(id, true);
|
||||||
@ -264,6 +260,13 @@ export default class Repository {
|
|||||||
return commit;
|
return commit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getHead() {
|
||||||
|
if (!(await this.#store.has("HEAD"))) return undefined;
|
||||||
|
const head = new TextDecoder().decode(await this.#store.get("HEAD"));
|
||||||
|
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
async readHead(): Promise<Commit | undefined> {
|
async readHead(): Promise<Commit | undefined> {
|
||||||
if (!(await this.#store.has("HEAD"))) return undefined;
|
if (!(await this.#store.has("HEAD"))) return undefined;
|
||||||
const head = new TextDecoder().decode(await this.#store.get("HEAD"));
|
const head = new TextDecoder().decode(await this.#store.get("HEAD"));
|
||||||
@ -272,8 +275,36 @@ export default class Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async mergeCommits(commit1: string, commit2: string): Promise<string> {
|
async mergeCommits(commit1: string, commit2: string): Promise<string> {
|
||||||
throw new Error("WIP");
|
// throw new Error("WIP");
|
||||||
// let newCommit = this.makeCommit()
|
// Find common history (if there). Diff changes.
|
||||||
|
|
||||||
|
const getCommitPath = async (commitID: string) => {
|
||||||
|
let commits: string[] = [];
|
||||||
|
let com = await this.readCommit(commitID)
|
||||||
|
if (com.before) {
|
||||||
|
commits = [...await getCommitPath(com.before), com.before]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (com.merge) { //TODO: Might cause duplicates?
|
||||||
|
commits = [...await getCommitPath(com.merge), com.merge];
|
||||||
|
}
|
||||||
|
|
||||||
|
return commits;
|
||||||
|
}
|
||||||
|
|
||||||
|
let commits1 = await getCommitPath(commit1);
|
||||||
|
let commitSet2 = new Set(await getCommitPath(commit2));
|
||||||
|
|
||||||
|
let common = commits1.find(commit => commitSet2.has(commit));
|
||||||
|
if (!common) {
|
||||||
|
// Complete merge of trees
|
||||||
|
}
|
||||||
|
|
||||||
|
let newTree =
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let newCommit = this.makeCommit()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,7 +319,7 @@ export default class Repository {
|
|||||||
while (current.before || (till && current.id == till)) {
|
while (current.before || (till && current.id == till)) {
|
||||||
current = await this.readCommit(current.before);
|
current = await this.readCommit(current.before);
|
||||||
if (!current)
|
if (!current)
|
||||||
throw new Error("Repository sems damaged! Can't read commit!");
|
throw new Error("Repository seems damaged! Can't read commit!");
|
||||||
log.push(current)
|
log.push(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,64 +405,73 @@ export default class Repository {
|
|||||||
objectID = await this.writeObject(data, "blob");
|
objectID = await this.writeObject(data, "blob");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vr = this.getVirtualRepo()
|
||||||
|
|
||||||
|
let blob = data ? Blob.create(vr, data) : undefined;
|
||||||
|
|
||||||
const lock = await this.#store.getLock();
|
const lock = await this.#store.getLock();
|
||||||
//TODO: Improve need of locking.
|
//TODO: Improve need of locking.
|
||||||
try {
|
try {
|
||||||
const head = await this.readHead();
|
const head = await this.getHead();
|
||||||
|
let com: Commit;
|
||||||
|
if(head)
|
||||||
|
com = Commit.load(vr, head);
|
||||||
|
else
|
||||||
|
com = Commit.create(vr);
|
||||||
|
|
||||||
const makeTree = async (treeID: string | undefined, parts: string[]) => {
|
// const makeTree = async (treeID: string | undefined, parts: string[]) => {
|
||||||
let tree: TreeEntry[];
|
// let tree: TreeEntry[];
|
||||||
if (treeID) {
|
// if (treeID) {
|
||||||
tree = await this.readTree(treeID);
|
// tree = await this.readTree(treeID);
|
||||||
} else {
|
// } else {
|
||||||
tree = [];
|
// tree = [];
|
||||||
}
|
// }
|
||||||
|
|
||||||
const current = parts[0];
|
// const current = parts[0];
|
||||||
|
|
||||||
let existing = tree.findIndex(([, , name]) => name == current);
|
// let existing = tree.findIndex(([, , name]) => name == current);
|
||||||
|
|
||||||
let entry: TreeEntry | undefined = undefined;
|
// let entry: TreeEntry | undefined = undefined;
|
||||||
|
|
||||||
if (parts.length == 1) {
|
// if (parts.length == 1) {
|
||||||
if (objectID) {
|
// if (objectID) {
|
||||||
entry = ["blob", objectID, current];
|
// entry = ["blob", objectID, current];
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
let newTreeID = await makeTree(existing >= 0 ? tree[existing][1] : undefined, parts.slice(1));
|
// let newTreeID = await makeTree(existing >= 0 ? tree[existing][1] : undefined, parts.slice(1));
|
||||||
if (newTreeID) {
|
// if (newTreeID) {
|
||||||
entry = [
|
// entry = [
|
||||||
"tree",
|
// "tree",
|
||||||
newTreeID,
|
// newTreeID,
|
||||||
current,
|
// current,
|
||||||
];
|
// ];
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (existing >= 0) {
|
// if (existing >= 0) {
|
||||||
let ex = tree[existing];
|
// let ex = tree[existing];
|
||||||
if (parts.length == 1 && ex[0] == "tree")
|
// if (parts.length == 1 && ex[0] == "tree")
|
||||||
throw new Error("This change would overwrite a folder!");
|
// throw new Error("This change would overwrite a folder!");
|
||||||
if (entry)
|
// if (entry)
|
||||||
tree[existing] = entry;
|
// tree[existing] = entry;
|
||||||
else {
|
// else {
|
||||||
tree = [...tree.slice(0, existing), ...tree.slice(existing + 1)];
|
// tree = [...tree.slice(0, existing), ...tree.slice(existing + 1)];
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
if (entry)
|
// if (entry)
|
||||||
tree.push(entry);
|
// tree.push(entry);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (tree.length > 0) {
|
// if (tree.length > 0) {
|
||||||
let treeString = tree.map(([type, hash, name]) => `${type} ${hash} ${name}`).join("\n");
|
// let treeString = tree.map(([type, hash, name]) => `${type} ${hash} ${name}`).join("\n");
|
||||||
|
|
||||||
let newTreeID = await this.writeObject(treeString, "tree");
|
// let newTreeID = await this.writeObject(treeString, "tree");
|
||||||
|
|
||||||
return newTreeID;
|
// return newTreeID;
|
||||||
} else {
|
// } else {
|
||||||
return undefined;
|
// return undefined;
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
let newTree = await makeTree(head?.root, parts);
|
let newTree = await makeTree(head?.root, parts);
|
||||||
if (!newTree) { //TODO: Is this what i want?
|
if (!newTree) { //TODO: Is this what i want?
|
||||||
@ -448,5 +488,293 @@ export default class Repository {
|
|||||||
if (this.#store.close) return this.#store?.close();
|
if (this.#store.close) return this.#store?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async applyVirtualCommit() {
|
||||||
|
//TODO: Maybe this??
|
||||||
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class VirtualRepo {
|
||||||
|
#virtualObjects = new Map<string, Uint8Array>();
|
||||||
|
constructor(protected repo: Repository) { }
|
||||||
|
|
||||||
|
async getObject(id: string): Promise<TypedObjectData | undefined> {
|
||||||
|
let local = this.#virtualObjects.get(id);
|
||||||
|
if (local)
|
||||||
|
return Repository.parseObject(local);
|
||||||
|
|
||||||
|
return this.repo.readObjectTyped(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async putObject(data: Uint8Array | string, type: ObjectTypeNames): Promise<ObjectID> {
|
||||||
|
let [id, d] = Repository.buildObject(data, type);
|
||||||
|
this.#virtualObjects.set(id, d);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async write() {
|
||||||
|
//TODO: Implement or use applyVirtualCommit of repo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectTuple = [string, Uint8Array];
|
||||||
|
|
||||||
|
abstract class RepoObject {
|
||||||
|
public abstract getObject(): Promise<[ObjectID, ObjectID[]] | null>;
|
||||||
|
public abstract freeze(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Blob extends RepoObject {
|
||||||
|
#id: ObjectID;
|
||||||
|
|
||||||
|
get id(): ObjectID {
|
||||||
|
return this.#id;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private vr: VirtualRepo, id: string) {
|
||||||
|
super();
|
||||||
|
this.#id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObject() {
|
||||||
|
this.freeze();
|
||||||
|
return [this.#id, []] as [ObjectID, ObjectID[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
freeze( ) {
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load(vr: VirtualRepo, id: string) {
|
||||||
|
return new Blob(vr, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(vr: VirtualRepo, data: Uint8Array | string) {
|
||||||
|
let id = await vr.putObject(data, "blob")
|
||||||
|
return new Blob(vr, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tree extends RepoObject {
|
||||||
|
trees: Map<string, Tree> = new Map();
|
||||||
|
blobs: Map<string, Blob> = new Map();
|
||||||
|
|
||||||
|
constructor(private vr: VirtualRepo) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load(vr: VirtualRepo, id: string) {
|
||||||
|
const res = await vr.getObject(id);
|
||||||
|
if (!res)
|
||||||
|
throw new Error("Invalid TreeID")
|
||||||
|
if (res.type !== "tree")
|
||||||
|
throw new Error("This item is not a tree!");
|
||||||
|
|
||||||
|
return this.parseTree(vr, res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(vr: VirtualRepo) {
|
||||||
|
return new Tree(vr);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTree(name: string, tree: Tree | null) {
|
||||||
|
if (this.blobs.has(name))
|
||||||
|
this.blobs.delete(name); // TODO: Think if error or silent overwrite
|
||||||
|
if (!tree)
|
||||||
|
this.trees.delete(name);
|
||||||
|
else
|
||||||
|
this.trees.set(name, tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlob(name: string, blob: Blob | null) {
|
||||||
|
if (this.trees.has(name))
|
||||||
|
this.trees.delete(name); // TODO: Think if error or silent overwrite
|
||||||
|
if (!blob)
|
||||||
|
return this.blobs.delete(name);
|
||||||
|
else
|
||||||
|
this.blobs.set(name, blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
freeze() {
|
||||||
|
Object.freeze(this.blobs)
|
||||||
|
Object.freeze(this.trees)
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObject(): Promise<[string, string[]] | null> {
|
||||||
|
this.freeze();
|
||||||
|
if (this.trees.size <= 0 && this.blobs.size <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
let trees = (await Promise.all([...this.trees.entries()].sort(([k1], [k2]) => {
|
||||||
|
if (k1 < k2)
|
||||||
|
return -1;
|
||||||
|
if (k2 > k2)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}).map(async ([name, element]) => {
|
||||||
|
let object = await element.getObject();
|
||||||
|
if (!object)
|
||||||
|
return undefined;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
id: object[0],
|
||||||
|
dependencies: object[1]
|
||||||
|
}
|
||||||
|
}))).filter(e => !!e) as { name: string, id: string, dependencies: string[] }[];
|
||||||
|
|
||||||
|
let blobs = (await Promise.all([...this.blobs.entries()].sort(([k1], [k2]) => {
|
||||||
|
if (k1 < k2)
|
||||||
|
return -1;
|
||||||
|
if (k2 > k2)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}).map(async ([name, element]) => {
|
||||||
|
let object = await element.getObject();
|
||||||
|
if (!object)
|
||||||
|
return undefined;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
id: object[0],
|
||||||
|
dependencies: object[1]
|
||||||
|
}
|
||||||
|
}))).filter(e => !!e) as { name: string, id: string, dependencies: string[] }[];
|
||||||
|
|
||||||
|
|
||||||
|
let dependencies = [
|
||||||
|
...trees.map(e => e?.id),
|
||||||
|
...trees.map(e => e?.dependencies).flat(1),
|
||||||
|
...blobs.map(e => e?.id),
|
||||||
|
...blobs.map(e => e?.dependencies).flat(1),
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
let rows: string[] = [];
|
||||||
|
trees.forEach((tree) => {
|
||||||
|
rows.push(`tree ${tree.id} ${tree.name}`);
|
||||||
|
})
|
||||||
|
|
||||||
|
blobs.forEach((blob) => {
|
||||||
|
rows.push(`blob ${blob.id} ${blob.name}`);
|
||||||
|
})
|
||||||
|
|
||||||
|
let content = rows.join("\n");
|
||||||
|
let newID = await this.vr.putObject(content, "tree");
|
||||||
|
|
||||||
|
|
||||||
|
//TODO:
|
||||||
|
// 1. Get Subtrees and their new IDs
|
||||||
|
// 2. Emit Subtrees/Blobs and new version of itself
|
||||||
|
|
||||||
|
return [newID, dependencies];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async parseTree(vr: VirtualRepo, treeStr: string | Uint8Array) {
|
||||||
|
if (typeof treeStr !== "string")
|
||||||
|
treeStr = new TextDecoder().decode(treeStr);
|
||||||
|
|
||||||
|
let tree = new Tree(vr);
|
||||||
|
|
||||||
|
await Promise.all(treeStr.split("\n").filter(e => e !== "").map(async (e) => {
|
||||||
|
const entry = e.split(" ") as TreeEntry;
|
||||||
|
const [type, hash, filename] = entry;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "blob":
|
||||||
|
tree.blobs.set(filename, await Blob.load(vr, hash));
|
||||||
|
break;
|
||||||
|
case "tree":
|
||||||
|
tree.trees.set(filename, await Tree.load(vr, hash));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid tree type."); //Might be a newer version or so
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Commit extends RepoObject {
|
||||||
|
root?: Tree = undefined;
|
||||||
|
before?: string = undefined;
|
||||||
|
date?: Date = undefined;
|
||||||
|
merge?: string;
|
||||||
|
|
||||||
|
constructor(private vr: VirtualRepo) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
freeze() {
|
||||||
|
Object.freeze(this.date);
|
||||||
|
Object.freeze(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObject() {
|
||||||
|
if (!this.root)
|
||||||
|
throw new Error("This commit has no root!");
|
||||||
|
|
||||||
|
if (!this.date)
|
||||||
|
this.date = new Date();
|
||||||
|
|
||||||
|
this.freeze()
|
||||||
|
const rootData = await this.root.getObject();
|
||||||
|
|
||||||
|
if (!rootData) //TODO: Might actually happen right now, when the last file is deleted
|
||||||
|
throw new Error("Invalid tree!")
|
||||||
|
|
||||||
|
const [treeID, dependencies] = rootData;
|
||||||
|
|
||||||
|
let commitStr =
|
||||||
|
`tree ${treeID}\ndate ${new Date().toISOString()}\n`;
|
||||||
|
if (this.before) {
|
||||||
|
commitStr += `before ${this.before}\n`;
|
||||||
|
if (this.merge) {
|
||||||
|
commitStr += `merge ${this.merge}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let commitID = await this.vr.putObject(commitStr, "comm");
|
||||||
|
|
||||||
|
return [commitID, [...dependencies, treeID]] as [string, string[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
static load(vr: VirtualRepo, id: string) {
|
||||||
|
let res = await vr.getObject(id);
|
||||||
|
if (!res)
|
||||||
|
throw new Error("Invalid ID");
|
||||||
|
|
||||||
|
let commitStr = new TextDecoder().decode(res.data);
|
||||||
|
|
||||||
|
let commit = new Commit(vr);
|
||||||
|
|
||||||
|
for (const entry of commitStr.split("\n")) {
|
||||||
|
const [type] = entry.split(" ", 1);
|
||||||
|
const value = entry.slice(type.length + 1);
|
||||||
|
switch (type) {
|
||||||
|
case "tree": // TODO: Simple validity checks
|
||||||
|
commit.root = await Tree.load(vr, value);
|
||||||
|
break;
|
||||||
|
case "before": // TODO: Simple validity checks
|
||||||
|
commit.before = value;
|
||||||
|
break;
|
||||||
|
case "merge": // TODO: Simple validity checks
|
||||||
|
commit.merge = value;
|
||||||
|
break;
|
||||||
|
case "date":
|
||||||
|
commit.date = new Date(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(vr: VirtualRepo) {
|
||||||
|
return new Commit(vr);
|
||||||
|
}
|
||||||
|
}
|
25
src/repo2.ts
Normal file
25
src/repo2.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
import { compose, multiply, prop, sumBy, map, pipe, pluck, filter, lt } from 'lodash/fp';
|
||||||
|
|
||||||
|
|
||||||
|
class Object {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Blob {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tree {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Commit {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Repo {
|
||||||
|
|
||||||
|
}
|
51
src/sync.ts
51
src/sync.ts
@ -6,7 +6,7 @@ import { ServiceProvider as ServiceProviderClient } from "./com/service_client";
|
|||||||
import { ServiceProvider as ServiceProviderServer } from "./com/service_server";
|
import { ServiceProvider as ServiceProviderServer } from "./com/service_server";
|
||||||
import { PullObjectRequest, PullObjectResponse, PushObjectRequest, PushObjectResponse, Sync as SyncClient, SyncInitRequest, SyncInitResponse } from "./com/sync_client";
|
import { PullObjectRequest, PullObjectResponse, PushObjectRequest, PushObjectResponse, Sync as SyncClient, SyncInitRequest, SyncInitResponse } from "./com/sync_client";
|
||||||
import { Sync as SyncServer } from "./com/sync_server";
|
import { Sync as SyncServer } from "./com/sync_server";
|
||||||
import Repository, { ObjectTypes } from "./repo";
|
import Repository, { Commit, ObjectTypes, TreeEntry } from "./repo";
|
||||||
|
|
||||||
if (typeof btoa === 'undefined') {
|
if (typeof btoa === 'undefined') {
|
||||||
global.btoa = function (str) {
|
global.btoa = function (str) {
|
||||||
@ -189,10 +189,53 @@ export async function startSyncClient(stream: ISimpleStream, repo: Repository) {
|
|||||||
|
|
||||||
let localHead = await repo.readHead()
|
let localHead = await repo.readHead()
|
||||||
if (localHead) { // If there is nothing to push, don't push
|
if (localHead) { // If there is nothing to push, don't push
|
||||||
// Find matching point
|
let toCheck: Commit[] = [localHead];
|
||||||
let match = undefined;
|
|
||||||
|
|
||||||
if (!syncResponse.remoteCommit) { }
|
let commit: Commit;
|
||||||
|
while(toCheck.length > 0) {
|
||||||
|
commit = toCheck.shift() as Commit;
|
||||||
|
if(commit.id != syncResponse.remoteCommit) { //Not yet at remote commit. Wait
|
||||||
|
commitsToPush.add(commit.id);
|
||||||
|
if(commit.before && !commitsToPush.has(commit.before)) // A commit could already be checked by the other part of a merge
|
||||||
|
toCheck.push(await repo.readCommit(commit.before));
|
||||||
|
|
||||||
|
if(commit.merge && !commitsToPush.has(commit.merge))
|
||||||
|
toCheck.push(await repo.readCommit(commit.merge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const traverseTree =async (current: TreeEntry[]) => {
|
||||||
|
for(const [type, hash, name] of current) {
|
||||||
|
objectsToPush.add(hash);
|
||||||
|
if(type =="tree") {
|
||||||
|
let tree = await repo.readTree(hash);
|
||||||
|
await traverseTree(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const commitId of commitsToPush) {
|
||||||
|
let commit = await repo.readCommit(commitId);
|
||||||
|
let rootId = commit.root
|
||||||
|
|
||||||
|
objectsToPush.add(rootId);
|
||||||
|
|
||||||
|
if(objectsToPush.has(rootId)) {
|
||||||
|
objectsToPush.add(rootId);
|
||||||
|
let root = await repo.readTree(rootId);
|
||||||
|
await traverseTree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const objId of objectsToPush) {
|
||||||
|
let obj = await repo.readObjectTyped(objId)
|
||||||
|
await service.PushObject({
|
||||||
|
id: objId,
|
||||||
|
data: obj?.data as Uint8Array,
|
||||||
|
type: obj?.type as string
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user