UNPKG

git-documentdb

Version:

Offline-first database that syncs with Git

757 lines 31.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.threeWayMerge = exports.merge = void 0; const path_1 = __importStar(require("path")); const isomorphic_git_1 = __importDefault(require("isomorphic-git")); const fs_extra_1 = __importDefault(require("fs-extra")); const const_1 = require("../const"); const error_1 = require("../error"); const worker_utils_1 = require("./worker_utils"); const utils_1 = require("../utils"); const blob_1 = require("../crud/blob"); /** * @internal */ function getStrategy(strategy, oursDoc, theirsDoc) { const defaultStrategy = const_1.DEFAULT_CONFLICT_RESOLUTION_STRATEGY; if (strategy === undefined) { strategy = defaultStrategy; } else if (strategy !== 'ours-diff' && strategy !== 'theirs-diff' && strategy !== 'ours' && strategy !== 'theirs') { // Strategy may be a function strategy = strategy(oursDoc, theirsDoc); if (strategy === undefined) { strategy = defaultStrategy; } } return strategy; } /** * @throws {@link Err.InvalidConflictResolutionStrategyError} * * @internal */ function getMergedJsonDoc(jsonDiff, jsonPatch, strategy, base, ours, theirs, serializeFormat) { let result; if (strategy === 'ours') { result = ours; } else if (strategy === 'theirs') { result = theirs; } else if (strategy === 'ours-diff') { result = jsonPatch.patch(ours, jsonDiff.diff(base, ours), theirs, jsonDiff.diff(base, theirs), strategy); } else if (strategy === 'theirs-diff') { result = jsonPatch.patch(ours, jsonDiff.diff(base, ours), theirs, jsonDiff.diff(base, theirs), strategy); } else { throw new error_1.Err.InvalidConflictResolutionStrategyError(); } return serializeFormat.serialize(result).data; } /** * @throws {@link Err.InvalidConflictResolutionStrategyError} * * @internal */ function getMergedTextDoc(strategy, base, ours, theirs) { if (strategy === 'ours') { return ours; } else if (strategy === 'theirs') { return theirs; } else if (strategy === 'ours-diff') { // TODO: implement diff and patch return ours; } else if (strategy === 'theirs-diff') { // TODO: implement diff and patch return theirs; } throw new error_1.Err.InvalidConflictResolutionStrategyError(); } /** * @throws {@link Err.InvalidConflictResolutionStrategyError} * * @internal */ function getMergedBinaryDoc(strategy, ours, theirs) { if (strategy === 'ours') { return ours; } else if (strategy === 'theirs') { return theirs; } throw new error_1.Err.InvalidConflictResolutionStrategyError(); } /** * getMergedDocument * * @throws {@link Err.InvalidDocTypeError} * * @throws # Errors from getMergedJsonDoc, getMergedTextDoc, getMergedBinaryDoc * @throws - {@link Err.InvalidConflictResolutionStrategyError} * * @internal */ function getMergedDocument(jsonDiff, jsonPatch, strategy, base, ours, theirs, docType, serializeFormat, extension = '') { if (docType === 'json') { const oursDoc = (0, blob_1.textToJsonDoc)((0, utils_1.utf8decode)(ours), serializeFormat, extension); const theirsDoc = (0, blob_1.textToJsonDoc)((0, utils_1.utf8decode)(theirs), serializeFormat, extension); let baseDoc; if (base) { baseDoc = (0, blob_1.textToJsonDoc)((0, utils_1.utf8decode)(base), serializeFormat, extension); } else { baseDoc = undefined; } return getMergedJsonDoc(jsonDiff, jsonPatch, strategy, baseDoc, oursDoc, theirsDoc, serializeFormat); } else if (docType === 'text') { const oursDoc = (0, utils_1.utf8decode)(ours); const theirsDoc = (0, utils_1.utf8decode)(theirs); let baseDoc; if (base) { baseDoc = (0, utils_1.utf8decode)(base); } else { baseDoc = undefined; } return getMergedTextDoc(strategy, baseDoc, oursDoc, theirsDoc); } else if (docType === 'binary') { return getMergedBinaryDoc(strategy, ours, theirs); } throw new error_1.Err.InvalidDocTypeError(docType); } /** * merge * * @throws Errors from {@link threeWayMerge} * * @internal */ async function merge(gitDDB, sync, baseCommitOid, oursCommitOid, theirsCommitOid) { const acceptedConflicts = []; const localChanges = []; const remoteChanges = []; const strategy = sync.options.conflictResolutionStrategy; const results = await isomorphic_git_1.default.walk({ fs: fs_extra_1.default, dir: gitDDB.workingDir, trees: [ isomorphic_git_1.default.TREE({ ref: baseCommitOid }), isomorphic_git_1.default.TREE({ ref: oursCommitOid }), isomorphic_git_1.default.TREE({ ref: theirsCommitOid }), ], // @ts-ignore // eslint-disable-next-line complexity map: async function (fullDocPath, [base, ours, theirs]) { const baseType = base === null ? undefined : await base.type(); if (baseType === 'tree') { return { mode: (await base.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: await base.oid(), type: await base.type(), }; } const oursType = ours === null ? undefined : await ours.type(); if (oursType === 'tree') { return { mode: (await ours.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: await ours.oid(), type: await ours.type(), }; } const theirsType = theirs === null ? undefined : await theirs.type(); if (theirsType === 'tree') { return { mode: (await theirs.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: await theirs.oid(), type: await theirs.type(), }; } const [treeEntry, localChange, remoteChange, conflict] = await threeWayMerge(gitDDB, sync, strategy, fullDocPath, base, ours, theirs); if (localChange !== undefined) { localChanges.push(localChange); } if (remoteChange !== undefined) { remoteChanges.push(remoteChange); } if (conflict !== undefined) { acceptedConflicts.push(conflict); } if (treeEntry === undefined) { return; } return treeEntry; }, reduce: async (parent, children) => { if (!parent) return; if (parent.type === 'tree') { if (children.length === 0) return; const newTreeOid = await isomorphic_git_1.default.writeTree({ fs: fs_extra_1.default, dir: gitDDB.workingDir, tree: children, }); // eslint-disable-next-line require-atomic-updates parent.oid = newTreeOid; } return parent; }, }); const mergedTreeOid = results.oid; // Entries in walk are traversed in alphabetical order // (https://isomorphic-git.org/docs/en/walk), // but sometimes the order collapse. // eslint-disable-next-line complexity const sortChangedFile = (a, b) => { if (a.operation === 'insert' && b.operation === 'insert') { if (a.new.name > b.new.name) return 1; if (a.new.name < b.new.name) return -1; return 0; } else if ((a.operation === 'update' || a.operation === 'delete') && (b.operation === 'update' || b.operation === 'delete')) { if (a.old.name > b.old.name) return 1; if (a.old.name < b.old.name) return -1; return 0; } // This line must not be reached. return 0; }; localChanges.sort(sortChangedFile); remoteChanges.sort(sortChangedFile); return [mergedTreeOid, localChanges, remoteChanges, acceptedConflicts]; } exports.merge = merge; /** * 3-way merge * * @throws {@link Err.InvalidConflictStateError} * @throws {@link Err.CannotDeleteDataError} * * @throws # Errors from getMergedDocument * @throws - {@link Err.InvalidDocTypeError} * @throws - {@link Err.InvalidConflictResolutionStrategyError} * * @throws # Errors from writeBlobToFile * @throws - {@link Err.CannotCreateDirectoryError} * * @throws # Errors from getFatDocFromData, getFatDocFromReadBlobResult * @throws - {@link Err.InvalidJsonObjectError} * */ // eslint-disable-next-line complexity async function threeWayMerge(gitDDB, sync, conflictResolutionStrategy, fullDocPath, base, ours, theirs) { const docType = gitDDB.serializeFormat.hasObjectExtension(fullDocPath) ? 'json' : 'text'; if (docType === 'text') { // TODO: select binary or text by .gitattribtues } // 2 x 2 x 2 cases if (!base && !ours && !theirs) { // This case must not occurred. throw new error_1.Err.InvalidConflictStateError('Neither a base entry nor a local entry nor a remote entry exists.'); } else if (!base && !ours && theirs) { // A new file has been inserted into theirs. // Write it to the working directory. // Write it to the index. // console.log(' #case 1 - Accept theirs (insert): ' + fullDocPath); const theirsData = (await theirs.content()); const theirsFatDoc = await (0, worker_utils_1.getFatDocFromData)(theirsData, fullDocPath, docType, gitDDB.serializeFormat); await (0, worker_utils_1.writeBlobToFile)(gitDDB.workingDir, fullDocPath, theirsData); await isomorphic_git_1.default.add({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); return [ { mode: (await theirs.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: await theirs.oid(), type: 'blob', }, { operation: 'insert', new: theirsFatDoc, }, undefined, undefined, ]; } else if (!base && ours && !theirs) { // A new file has been inserted into ours. // It has already been created on the working directory. // It has already been added to the index. // console.log(' #case 2 - Accept ours (insert): ' + fullDocPath); const oursData = (await ours.content()); const oursFatDoc = await (0, worker_utils_1.getFatDocFromData)(oursData, fullDocPath, docType, gitDDB.serializeFormat); return [ { mode: (await ours.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: await ours.oid(), type: 'blob', }, undefined, { operation: 'insert', new: oursFatDoc, }, undefined, ]; } else if (!base && ours && theirs) { const oursOid = await ours.oid(); const theirsOid = await theirs.oid(); const oursMode = (await ours.mode()).toString(8); const theirsMode = (await theirs.mode()).toString(8); if (oursOid === theirsOid && oursMode === theirsMode) { // The same filenames with exactly the same contents are inserted on both local and remote. // It has already been created on the both working directory. // It has already been added to the both index. // console.log(' #case 3 - Accept both (insert): ' + fullDocPath); return [ { mode: oursMode, path: (0, path_1.basename)(fullDocPath), oid: oursOid, type: 'blob', }, undefined, undefined, undefined, ]; } // ! Conflict const oursData = (await ours.content()); const theirsData = (await theirs.content()); const oursFatDoc = await (0, worker_utils_1.getFatDocFromData)(oursData, fullDocPath, docType, gitDDB.serializeFormat); const theirsFatDoc = await (0, worker_utils_1.getFatDocFromData)(theirsData, fullDocPath, docType, gitDDB.serializeFormat); const strategy = await getStrategy(conflictResolutionStrategy, oursFatDoc, theirsFatDoc); let mode = ''; if (strategy === 'ours' || strategy === 'ours-diff') { // console.log(' #case 4 - Conflict. Accept ours (insert): ' + fullDocPath); mode = oursMode; } else if (strategy === 'theirs' || strategy === 'theirs-diff') { // console.log(' #case 5 - Conflict. Accept theirs (insert): ' + fullDocPath); mode = theirsMode; } let resultFatDoc; let localChange; let remoteChange; if (strategy === 'ours') { // Can skip getMergedDocument(). resultFatDoc = oursFatDoc; localChange = undefined; remoteChange = { operation: 'update', old: theirsFatDoc, new: oursFatDoc, }; } else if (strategy === 'theirs') { // Can skip getMergedDocument(). resultFatDoc = theirsFatDoc; await (0, worker_utils_1.writeBlobToFile)(gitDDB.workingDir, fullDocPath, theirsData); await isomorphic_git_1.default.add({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); localChange = { operation: 'update', old: oursFatDoc, new: theirsFatDoc, }; remoteChange = undefined; } else { // Diff and patch const extensionMatch = fullDocPath.match(/.+(\..+?)$/); let extension = ''; if (extensionMatch) { extension = extensionMatch[1]; } const data = await getMergedDocument(sync.jsonDiff, sync.jsonPatch, strategy, undefined, oursData, theirsData, docType, gitDDB.serializeFormat, extension); resultFatDoc = await (0, worker_utils_1.getFatDocFromData)(data, fullDocPath, docType, gitDDB.serializeFormat); await (0, worker_utils_1.writeBlobToFile)(gitDDB.workingDir, fullDocPath, data); await isomorphic_git_1.default.add({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); if (!(0, blob_1.isSameFatDoc)(oursFatDoc, resultFatDoc)) { localChange = { operation: 'update', old: oursFatDoc, new: resultFatDoc, }; } if (!(0, blob_1.isSameFatDoc)(theirsFatDoc, resultFatDoc)) { remoteChange = { operation: 'update', old: theirsFatDoc, new: resultFatDoc, }; } } const acceptedConflict = { fatDoc: resultFatDoc, strategy, operation: strategy.endsWith('-diff') ? 'insert-merge' : 'insert', }; return [ { mode, path: (0, path_1.basename)(fullDocPath), oid: resultFatDoc.fileOid, type: 'blob', }, localChange, remoteChange, acceptedConflict, ]; } else if (base && !ours && !theirs) { // The same files have been removed from both local and remote. // console.log(' #case 6 - Accept both (delete): ' + fullDocPath); return [undefined, undefined, undefined, undefined]; } else if (base && !ours && theirs) { const baseOid = await base.oid(); const theirsOid = await theirs.oid(); const theirsData = (await theirs.content()); const theirsFatDoc = await (0, worker_utils_1.getFatDocFromData)(theirsData, fullDocPath, docType, gitDDB.serializeFormat); if (baseOid === theirsOid) { // A file has been removed from ours. // console.log(' #case 7 - Accept ours (delete): ' + fullDocPath); return [ undefined, undefined, { operation: 'delete', old: theirsFatDoc, }, undefined, ]; } // ! Conflict const strategy = await getStrategy(conflictResolutionStrategy, undefined, theirsFatDoc); if (strategy === 'ours' || strategy === 'ours-diff') { // console.log(' #case 8 - Conflict. Accept ours (delete): ' + fullDocPath); const baseData = (await base.content()); const baseFatDoc = await (0, worker_utils_1.getFatDocFromData)(baseData, fullDocPath, docType, gitDDB.serializeFormat); const acceptedConflict = { fatDoc: baseFatDoc, strategy: strategy, operation: 'delete', }; return [ undefined, undefined, { operation: 'delete', old: theirsFatDoc, }, acceptedConflict, ]; } else if (strategy === 'theirs' || strategy === 'theirs-diff') { // console.log(' #case 9 - Conflict. Accept theirs (update): ' + fullDocPath); const acceptedConflict = { fatDoc: theirsFatDoc, strategy: strategy, operation: 'update', }; await (0, worker_utils_1.writeBlobToFile)(gitDDB.workingDir, fullDocPath, theirsData); await isomorphic_git_1.default.add({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); return [ { mode: (await theirs.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: theirsFatDoc.fileOid, type: 'blob', }, { operation: 'insert', new: theirsFatDoc, }, undefined, acceptedConflict, ]; } } else if (base && ours && !theirs) { const baseOid = await base.oid(); const oursOid = await ours.oid(); const oursData = (await ours.content()); const oursFatDoc = await (0, worker_utils_1.getFatDocFromData)(oursData, fullDocPath, docType, gitDDB.serializeFormat); if (baseOid === oursOid) { // A file has been removed from theirs. // console.log(' #case 10 - Accept theirs (delete): ' + fullDocPath); await fs_extra_1.default.remove(path_1.default.resolve(gitDDB.workingDir, fullDocPath)).catch(() => { throw new error_1.Err.CannotDeleteDataError(); }); await isomorphic_git_1.default.remove({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); return [ undefined, { operation: 'delete', old: oursFatDoc, }, undefined, undefined, ]; } // ! Conflict const strategy = await getStrategy(conflictResolutionStrategy, oursFatDoc, undefined); if (strategy === 'ours' || strategy === 'ours-diff') { // console.log(' #case 11 - Conflict. Accept ours (update): ' + fullDocPath); const acceptedConflict = { fatDoc: oursFatDoc, strategy: strategy, operation: 'update', }; return [ { mode: (await ours.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: oursOid, type: 'blob', }, undefined, { operation: 'insert', new: oursFatDoc, }, acceptedConflict, ]; } else if (strategy === 'theirs' || strategy === 'theirs-diff') { // console.log(' #case 12 - Conflict. Accept theirs (delete): ' + fullDocPath); const baseData = (await base.content()); const baseFatDoc = await (0, worker_utils_1.getFatDocFromData)(baseData, fullDocPath, docType, gitDDB.serializeFormat); const acceptedConflicts = { fatDoc: baseFatDoc, strategy: strategy, operation: 'delete', }; await fs_extra_1.default.remove(path_1.default.resolve(gitDDB.workingDir, fullDocPath)).catch(() => { throw new error_1.Err.CannotDeleteDataError(); }); await isomorphic_git_1.default.remove({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); return [ undefined, { operation: 'delete', old: oursFatDoc, }, undefined, acceptedConflicts, ]; } } else if (base && ours && theirs) { const baseOid = await base.oid(); const oursOid = await ours.oid(); const theirsOid = await theirs.oid(); if (oursOid === theirsOid) { // The same filenames with exactly the same contents are inserted into both local and remote. // console.log(' #case 13 - Accept both (update): ' + fullDocPath); return [ { // TODO: check whether mode is the same. mode: (await ours.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: oursOid, type: 'blob', }, undefined, undefined, undefined, ]; } else if (baseOid === oursOid) { // console.log(' #case 14 - Accept theirs (update): ' + fullDocPath); const oursData = (await ours.content()); const oursFatDoc = await (0, worker_utils_1.getFatDocFromData)(oursData, fullDocPath, docType, gitDDB.serializeFormat); const theirsData = (await theirs.content()); const theirsFatDoc = await (0, worker_utils_1.getFatDocFromData)(theirsData, fullDocPath, docType, gitDDB.serializeFormat); await (0, worker_utils_1.writeBlobToFile)(gitDDB.workingDir, fullDocPath, theirsData); await isomorphic_git_1.default.add({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); return [ { mode: (await theirs.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: theirsFatDoc.fileOid, type: 'blob', }, { operation: 'update', old: oursFatDoc, new: theirsFatDoc, }, undefined, undefined, ]; } else if (baseOid === theirsOid) { // console.log(' #case 15 - Accept ours (update): ' + fullDocPath); const oursData = (await ours.content()); const oursFatDoc = await (0, worker_utils_1.getFatDocFromData)(oursData, fullDocPath, docType, gitDDB.serializeFormat); const theirsData = (await theirs.content()); const theirsFatDoc = await (0, worker_utils_1.getFatDocFromData)(theirsData, fullDocPath, docType, gitDDB.serializeFormat); return [ { mode: (await theirs.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: oursFatDoc.fileOid, type: 'blob', }, undefined, { operation: 'update', old: theirsFatDoc, new: oursFatDoc, }, undefined, ]; } // ! Conflict const baseData = (await base.content()); const oursData = (await ours.content()); const theirsData = (await theirs.content()); const oursFatDoc = await (0, worker_utils_1.getFatDocFromData)(oursData, fullDocPath, docType, gitDDB.serializeFormat); const theirsFatDoc = await (0, worker_utils_1.getFatDocFromData)(theirsData, fullDocPath, docType, gitDDB.serializeFormat); const strategy = await getStrategy(conflictResolutionStrategy, oursFatDoc, theirsFatDoc); if (strategy === 'ours') { // console.log(' #case 16 - Conflict. Accept ours (update): ' + fullDocPath); return [ { mode: (await ours.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: oursFatDoc.fileOid, type: 'blob', }, undefined, { operation: 'update', old: theirsFatDoc, new: oursFatDoc, }, { fatDoc: oursFatDoc, strategy: strategy, operation: 'update', }, ]; } else if (strategy === 'theirs') { // console.log(' #case 17 - Conflict. Accept theirs (update): ' + fullDocPath); await (0, worker_utils_1.writeBlobToFile)(gitDDB.workingDir, fullDocPath, theirsData); await isomorphic_git_1.default.add({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); return [ { mode: (await theirs.mode()).toString(8), path: (0, path_1.basename)(fullDocPath), oid: theirsFatDoc.fileOid, type: 'blob', }, { operation: 'update', old: oursFatDoc, new: theirsFatDoc, }, undefined, { fatDoc: theirsFatDoc, strategy: strategy, operation: 'update', }, ]; } const extensionMatch = fullDocPath.match(/.+(\..+?)$/); let extension = ''; if (extensionMatch) { extension = extensionMatch[1]; } const data = await getMergedDocument(sync.jsonDiff, sync.jsonPatch, strategy, baseData, oursData, theirsData, docType, gitDDB.serializeFormat, extension); const resultFatDoc = await (0, worker_utils_1.getFatDocFromData)(data, fullDocPath, docType, gitDDB.serializeFormat); await (0, worker_utils_1.writeBlobToFile)(gitDDB.workingDir, fullDocPath, data); await isomorphic_git_1.default.add({ fs: fs_extra_1.default, dir: gitDDB.workingDir, filepath: fullDocPath }); let mode = ''; if (strategy === 'ours-diff') { // console.log(' #case 16 (diff) - Conflict. Accept ours (update): ' + fullDocPath); mode = (await ours.mode()).toString(8); } else if (strategy === 'theirs-diff') { // console.log(' #case 17 (diff) - Conflict. Accept theirs (update): ' + fullDocPath); mode = (await theirs.mode()).toString(8); } let localChange; let remoteChange; if (!(0, blob_1.isSameFatDoc)(oursFatDoc, resultFatDoc)) { localChange = { operation: 'update', old: oursFatDoc, new: resultFatDoc, }; } if (!(0, blob_1.isSameFatDoc)(theirsFatDoc, resultFatDoc)) { remoteChange = { operation: 'update', old: theirsFatDoc, new: resultFatDoc, }; } return [ { mode, path: (0, path_1.basename)(fullDocPath), oid: resultFatDoc.fileOid, type: 'blob', }, localChange, remoteChange, { fatDoc: resultFatDoc, strategy: strategy, operation: 'update-merge', }, ]; } throw new error_1.Err.InvalidConflictStateError('Invalid case'); } exports.threeWayMerge = threeWayMerge; //# sourceMappingURL=3way_merge.js.map