git-documentdb
Version:
Offline-first database that syncs with Git
279 lines • 11.5 kB
JavaScript
"use strict";
/* eslint-disable no-await-in-loop */
/**
* GitDocumentDB
* Copyright (c) Hidekazu Kubota
*
* This source code is licensed under the Mozilla Public License Version 2.0
* found in the LICENSE file in the root directory of gitDDB source tree.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.syncWorker = void 0;
const isomorphic_git_1 = __importDefault(require("isomorphic-git"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const const_1 = require("../const");
const utils_1 = require("../utils");
const push_worker_1 = require("./push_worker");
const worker_utils_1 = require("./worker_utils");
const _3way_merge_1 = require("./3way_merge");
const remote_engine_1 = require("./remote_engine");
const error_1 = require("../error");
/**
* sync_worker
*
* @throws {@link Err.NoMergeBaseFoundError}
* @throws {@link Err.ThreeWayMergeError}
* @throws {@link Err.CannotDeleteDataError}
*
* @throws # Errors from fetch, pushWorker
* @throws - {@link RemoteErr.InvalidGitRemoteError}
* @throws - {@link RemoteErr.InvalidURLFormatError}
* @throws - {@link RemoteErr.NetworkError}
* @throws - {@link RemoteErr.HTTPError401AuthorizationRequired}
* @throws - {@link RemoteErr.HTTPError404NotFound}
* @throws - {@link RemoteErr.CannotConnectError}
* @throws - {@link RemoteErr.HttpProtocolRequiredError}
* @throws - {@link RemoteErr.InvalidRepositoryURLError}
* @throws - {@link RemoteErr.InvalidSSHKeyPathError}
* @throws - {@link RemoteErr.InvalidAuthenticationTypeError}
*
* @throws # Errors from pushWorker
* @throws - {@link RemoteErr.HTTPError403Forbidden}
* @throws - {@link RemoteErr.UnfetchedCommitExistsError}
*
* @throws # Errors from 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}
*
* @throws # Errors from getChanges
* @throws - {@link Err.InvalidJsonObjectError}
*
* @internal
*/
// eslint-disable-next-line complexity
async function syncWorker(gitDDB, sync, taskMetadata) {
const syncOptions = sync.options;
sync.eventHandlers.start.forEach(listener => {
listener.func({ ...taskMetadata, collectionPath: listener.collectionPath }, sync.currentRetries());
});
/**
* Fetch
*/
await remote_engine_1.RemoteEngine[sync.engine]
.fetch(gitDDB.workingDir, syncOptions, sync.remoteName, gitDDB.defaultBranch, gitDDB.defaultBranch, gitDDB.tsLogger)
.catch(err => {
throw (0, remote_engine_1.wrappingRemoteEngineError)(err);
});
/**
* Calc distance
*/
const oldCommitOid = await isomorphic_git_1.default.resolveRef({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
ref: 'HEAD',
});
const oldRemoteCommitOid = await isomorphic_git_1.default.resolveRef({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
ref: `refs/remotes/${sync.remoteName}/${gitDDB.defaultBranch}`,
});
const [baseCommitOid] = await isomorphic_git_1.default.findMergeBase({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
oids: [oldCommitOid, oldRemoteCommitOid],
});
const distance = await (0, worker_utils_1.calcDistance)(baseCommitOid, oldCommitOid, oldRemoteCommitOid);
// ahead: 0, behind 0 => Nothing to do: Local does not have new commits. Remote has not pushed new commits.
// ahead: 0, behind 1 => Fast-forward merge : Local does not have new commits. Remote has pushed new commits.
// ahead: 1, behind 0 => Push : Local has new commits. Remote has not pushed new commits.
// ahead: 1, behind 1 => Merge, may resolve conflict and push: Local has new commits. Remote has pushed new commits.
if (distance.ahead === undefined || distance.behind === undefined) {
throw new error_1.Err.NoMergeBaseFoundError();
}
else if (distance.ahead === 0 && distance.behind === 0) {
return { action: 'nop' };
}
else if (distance.ahead === 0 && distance.behind > 0) {
/**
* Fast forward
*/
await isomorphic_git_1.default.writeRef({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
ref: 'refs/heads/' + gitDDB.defaultBranch,
value: oldRemoteCommitOid,
force: true,
});
const newCommitOid = oldRemoteCommitOid;
const localChanges = await (0, worker_utils_1.getAndWriteLocalChanges)(gitDDB.workingDir, oldCommitOid, newCommitOid, gitDDB.serializeFormat);
const syncResultFastForwardMerge = {
action: 'fast-forward merge',
changes: {
local: localChanges,
},
};
if (syncOptions.includeCommits) {
// Get list of commits which has been merged to local
const commitsFromRemote = await (0, worker_utils_1.getCommitLogs)(gitDDB.workingDir, oldRemoteCommitOid, oldCommitOid);
syncResultFastForwardMerge.commits = {
local: commitsFromRemote,
};
}
return syncResultFastForwardMerge;
}
else if (distance.ahead > 0 && distance.behind === 0) {
/**
* Push
*/
return await (0, push_worker_1.pushWorker)(gitDDB, sync, taskMetadata, true).catch(err => {
throw err;
});
}
/**
* Merge (distance.ahead > 0 && distance.behind > 0)
*/
const [mergedTreeOid, localChanges, remoteChanges, acceptedConflicts] = await (0, _3way_merge_1.merge)(gitDDB, sync, baseCommitOid, oldCommitOid, oldRemoteCommitOid);
if (acceptedConflicts.length === 0) {
const mergeCommitOid = await isomorphic_git_1.default.commit({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
author: gitDDB.author,
committer: gitDDB.committer,
parent: [oldCommitOid, oldRemoteCommitOid],
message: 'merge',
tree: mergedTreeOid,
});
let localCommits;
// Get list of commits which has been added to local
if (syncOptions.includeCommits) {
const mergeCommit = await isomorphic_git_1.default.readCommit({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
oid: mergeCommitOid,
});
const commitsFromRemote = await (0, worker_utils_1.getCommitLogs)(gitDDB.workingDir, oldRemoteCommitOid, baseCommitOid);
// Add merge commit
localCommits = [...commitsFromRemote, (0, utils_1.normalizeCommit)(mergeCommit)];
}
// Need push because it is merged normally.
const syncResultPush = (await (0, push_worker_1.pushWorker)(gitDDB, sync, taskMetadata, true, true).catch((err) => {
return err;
}));
if (syncResultPush instanceof Error) {
const syncResultMergeAndPushError = {
action: 'merge and push error',
changes: {
local: localChanges,
},
error: syncResultPush,
};
if (localCommits) {
syncResultMergeAndPushError.commits = {
local: localCommits,
};
}
return syncResultMergeAndPushError;
}
const syncResultMergeAndPush = {
action: 'merge and push',
changes: {
local: localChanges,
// remote: syncResultPush.changes.remote,
remote: remoteChanges,
},
};
if (localCommits) {
syncResultMergeAndPush.commits = {
local: localCommits,
remote: syncResultPush.commits.remote,
};
}
return syncResultMergeAndPush;
}
/**
* Conflict
* https://git-scm.com/docs/git-merge#_true_merge
*/
acceptedConflicts.sort((a, b) => {
return a.fatDoc.name === b.fatDoc.name ? 0 : a.fatDoc.name > b.fatDoc.name ? 1 : -1;
});
// console.log(acceptedConflicts);
let commitMessage = 'resolve: ';
acceptedConflicts.forEach(conflict => {
// e.g.) put-ours: myID
commitMessage += `${conflict.fatDoc.name}(${conflict.operation},${conflict.fatDoc.fileOid.substr(0, const_1.SHORT_SHA_LENGTH)},${conflict.strategy}), `;
});
if (commitMessage.endsWith(', ')) {
commitMessage = commitMessage.slice(0, -2);
}
const mergeCommitOid = await isomorphic_git_1.default.commit({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
author: gitDDB.author,
committer: gitDDB.committer,
parent: [oldCommitOid, oldRemoteCommitOid],
message: commitMessage,
tree: mergedTreeOid,
});
// const localChanges = await getChanges(gitDDB.workingDir, oldCommitOid, mergeCommitOid);
// Get list of commits which has been added to local
let localCommits;
if (syncOptions.includeCommits) {
const commitsFromRemote = await (0, worker_utils_1.getCommitLogs)(gitDDB.workingDir, oldRemoteCommitOid, baseCommitOid);
const overwriteCommit = await isomorphic_git_1.default.readCommit({
fs: fs_extra_1.default,
dir: gitDDB.workingDir,
oid: mergeCommitOid,
});
// Add merge commit
localCommits = [...commitsFromRemote, (0, utils_1.normalizeCommit)(overwriteCommit)];
}
// Push
const syncResultPush = (await (0, push_worker_1.pushWorker)(gitDDB, sync, taskMetadata, true, true).catch((err) => {
return err;
}));
if (syncResultPush instanceof Error) {
const syncResultResolveConflictsAndPushError = {
action: 'resolve conflicts and push error',
conflicts: acceptedConflicts,
changes: {
local: localChanges,
},
error: syncResultPush,
};
if (localCommits) {
syncResultResolveConflictsAndPushError.commits = {
local: localCommits,
};
}
return syncResultResolveConflictsAndPushError;
}
const syncResultResolveConflictsAndPush = {
action: 'resolve conflicts and push',
conflicts: acceptedConflicts,
changes: {
local: localChanges,
// remote: syncResultPush.changes.remote,
remote: remoteChanges,
},
};
if (localCommits) {
syncResultResolveConflictsAndPush.commits = {
local: localCommits,
remote: syncResultPush.commits.remote,
};
}
return syncResultResolveConflictsAndPush;
}
exports.syncWorker = syncWorker;
//# sourceMappingURL=sync_worker.js.map