UNPKG

git-documentdb-plugin-remote-nodegit

Version:

GitDocumentDB plugin for remote connection using NodeGit

514 lines 23.5 kB
"use strict"; /** * GitDocumentDB plugin for remote connection using NodeGit * 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 this source tree. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.push = exports.fetch = exports.checkFetch = exports.clone = exports.name = exports.type = exports.sleep = void 0; const fs_1 = __importDefault(require("fs")); // @ts-ignore const nodegit_1 = __importDefault(require("@sosuisen/nodegit")); const isomorphic_git_1 = __importDefault(require("isomorphic-git")); const tslog_1 = require("tslog"); const git_documentdb_remote_errors_1 = require("git-documentdb-remote-errors"); const authentication_1 = require("./authentication"); const NETWORK_RETRY = 3; const NETWORK_RETRY_INTERVAL = 1000; /** * @internal */ function sleep(msec) { return new Promise((resolve) => setTimeout(resolve, msec)); } exports.sleep = sleep; /** * @public */ // eslint-disable-next-line @typescript-eslint/naming-convention exports.type = 'remote'; /** * @public */ // eslint-disable-next-line @typescript-eslint/naming-convention exports.name = 'nodegit'; /** * Clone * * @throws {@link InvalidURLFormatError} * @throws {@link NetworkError} * @throws {@link HTTPError401AuthorizationRequired} * @throws {@link HTTPError404NotFound} * @throws {@link CannotConnectError} * * @throws {@link HttpProtocolRequiredError} (from createCredentialForGitHub) * @throws {@link InvalidRepositoryURLError} (from createCredentialForGitHub) * @throws {@link InvalidSSHKeyPathError} (from createCredentialForSSH) * * @throws {@link InvalidAuthenticationTypeError} (from createCredential) */ // eslint-disable-next-line complexity async function clone(workingDir, remoteOptions, remoteName = 'origin', logger) { var _a, _b; logger !== null && logger !== void 0 ? logger : (logger = new tslog_1.Logger({ name: 'plugin-nodegit', minLevel: 'trace', displayDateTime: false, displayFunctionName: false, displayFilePath: 'hidden', })); logger.debug(`remote-nodegit: clone: ${remoteOptions.remoteUrl}`); (_a = remoteOptions.retry) !== null && _a !== void 0 ? _a : (remoteOptions.retry = NETWORK_RETRY); (_b = remoteOptions.retryInterval) !== null && _b !== void 0 ? _b : (remoteOptions.retryInterval = NETWORK_RETRY_INTERVAL); for (let i = 0; i < remoteOptions.retry + 1; i++) { // eslint-disable-next-line no-await-in-loop const res = await nodegit_1.default.Clone.clone(remoteOptions.remoteUrl, workingDir, { fetchOpts: { callbacks: authentication_1.createCredentialCallback(remoteOptions), }, }).catch((err) => err); let error = ''; if (res instanceof Error) { error = res.message; } else { break; } // console.log('# error: ' + error); switch (true) { case error.startsWith('unsupported URL protocol'): case error.startsWith('malformed URL'): throw new git_documentdb_remote_errors_1.InvalidURLFormatError(error); // NodeGit throws them when network is limited. case error.startsWith('failed to send request'): case error.startsWith('failed to resolve address'): case error.startsWith('failed to connect'): // Ubuntu if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.NetworkError(error); } break; case error.startsWith('unexpected HTTP status code: 401'): // 401 on Ubuntu case error.startsWith('request failed with status code: 401'): // 401 on Windows case error.startsWith('remote authentication required but no callback set'): // Ubuntu case error.startsWith('could not find appropriate mechanism for credentials'): // Ubuntu case error.startsWith('Method connect has thrown an error'): case error.startsWith('remote credential provider returned an invalid cred type'): // on Ubuntu case error.startsWith('Failed to retrieve list of SSH authentication methods'): case error.startsWith('too many redirects or authentication replays'): throw new git_documentdb_remote_errors_1.HTTPError401AuthorizationRequired(error); case error.startsWith('unexpected http status code: 404'): // 404 on Ubuntu case error.startsWith('request failed with status code: 404'): // 404 on Windows throw new git_documentdb_remote_errors_1.HTTPError404NotFound(error); default: if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.CannotConnectError(error); } } // eslint-disable-next-line no-await-in-loop await sleep(remoteOptions.retryInterval); } // Add remote // (default is 'origin') if (remoteName !== 'origin') { await isomorphic_git_1.default.addRemote({ fs: fs_1.default, dir: workingDir, remote: 'origin', url: remoteOptions.remoteUrl, }); } await isomorphic_git_1.default.addRemote({ fs: fs_1.default, dir: workingDir, remote: remoteName, url: remoteOptions.remoteUrl, }); } exports.clone = clone; /** * Check connection by FETCH * * @throws {@link InvalidGitRemoteError} * @throws {@link InvalidURLFormatError} * @throws {@link NetworkError} * @throws {@link HTTPError401AuthorizationRequired} * @throws {@link HTTPError404NotFound} * @throws {@link CannotConnectError} * * @throws {@link HttpProtocolRequiredError} (from createCredentialForGitHub) * @throws {@link InvalidRepositoryURLError} (from createCredentialForGitHub) * @throws {@link InvalidSSHKeyPathError} (from createCredentialForSSH) * * @throws {@link InvalidAuthenticationTypeError} (from createCredential) * * @public */ // eslint-disable-next-line complexity async function checkFetch(workingDir, remoteOptions, remoteName = 'origin', logger) { var _a, _b; logger !== null && logger !== void 0 ? logger : (logger = new tslog_1.Logger({ name: 'plugin-nodegit', minLevel: 'trace', displayDateTime: false, displayFunctionName: false, displayFilePath: 'hidden', })); const callbacks = authentication_1.createCredentialCallback(remoteOptions); const repos = await nodegit_1.default.Repository.open(workingDir); const remote = await repos .getRemote(remoteName) .catch((err) => { if (/^remote '.+?' does not exist/.test(err.message)) { throw new git_documentdb_remote_errors_1.InvalidGitRemoteError(err.message); } throw err; }); (_a = remoteOptions.retry) !== null && _a !== void 0 ? _a : (remoteOptions.retry = NETWORK_RETRY); (_b = remoteOptions.retryInterval) !== null && _b !== void 0 ? _b : (remoteOptions.retryInterval = NETWORK_RETRY_INTERVAL); for (let i = 0; i < remoteOptions.retry + 1; i++) { const error = String( // eslint-disable-next-line no-await-in-loop await remote .connect(nodegit_1.default.Enums.DIRECTION.FETCH, callbacks) .catch((err) => err)); // eslint-disable-next-line no-await-in-loop await remote.disconnect(); if (error === 'undefined') { break; } console.log('# error: ' + error); switch (true) { case error.startsWith('Error: unsupported URL protocol'): case error.startsWith('Error: malformed URL'): throw new git_documentdb_remote_errors_1.InvalidURLFormatError(error); // NodeGit throws them when network is limited. case error.startsWith('Error: failed to send request'): case error.startsWith('Error: failed to resolve address'): case error.startsWith('Error: failed to connect'): // Ubuntu if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.NetworkError(error); } break; case error.startsWith('Error: unexpected HTTP status code: 401'): // 401 on Ubuntu case error.startsWith('Error: request failed with status code: 401'): // 401 on Windows case error.startsWith('Error: remote authentication required but no callback set'): // Ubuntu case error.startsWith('Error: could not find appropriate mechanism for credentials'): // Ubuntu case error.startsWith('Error: Method connect has thrown an error'): case error.startsWith('Error: remote credential provider returned an invalid cred type'): // on Ubuntu case error.startsWith('Failed to retrieve list of SSH authentication methods'): case error.startsWith('Error: too many redirects or authentication replays'): throw new git_documentdb_remote_errors_1.HTTPError401AuthorizationRequired(error); case error.startsWith('Error: unexpected http status code: 404'): // 404 on Ubuntu case error.startsWith('Error: request failed with status code: 404'): // 404 on Windows throw new git_documentdb_remote_errors_1.HTTPError404NotFound(error); default: if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.CannotConnectError(error); } } // eslint-disable-next-line no-await-in-loop await sleep(remoteOptions.retryInterval); } return true; } exports.checkFetch = checkFetch; /** * git fetch * * @throws {@link InvalidGitRemoteError} * @throws {@link InvalidURLFormatError} * @throws {@link NetworkError} * @throws {@link HTTPError401AuthorizationRequired} * @throws {@link HTTPError404NotFound} * @throws {@link CannotConnectError} * * @throws {@link HttpProtocolRequiredError} (from createCredentialForGitHub) * @throws {@link InvalidRepositoryURLError} (from createCredentialForGitHub) * @throws {@link InvalidSSHKeyPathError} (from createCredentialForSSH) * * @throws {@link InvalidAuthenticationTypeError} (from createCredential) * * @public */ // eslint-disable-next-line complexity async function fetch(workingDir, remoteOptions, remoteName, localBranchName, remoteBranchName, logger) { var _a, _b; logger !== null && logger !== void 0 ? logger : (logger = new tslog_1.Logger({ name: 'plugin-nodegit', minLevel: 'trace', displayDateTime: false, displayFunctionName: false, displayFilePath: 'hidden', })); logger.debug(`remote-nodegit: fetch: ${remoteOptions.remoteUrl}`); remoteName !== null && remoteName !== void 0 ? remoteName : (remoteName = 'origin'); localBranchName !== null && localBranchName !== void 0 ? localBranchName : (localBranchName = 'main'); remoteBranchName !== null && remoteBranchName !== void 0 ? remoteBranchName : (remoteBranchName = 'main'); const repos = await nodegit_1.default.Repository.open(workingDir); const remote = await repos .getRemote(remoteName) .catch((err) => { if (/^remote '.+?' does not exist/.test(err.message)) { throw new git_documentdb_remote_errors_1.InvalidGitRemoteError(err.message); } throw err; }); const callbacks = authentication_1.createCredentialCallback(remoteOptions); (_a = remoteOptions.retry) !== null && _a !== void 0 ? _a : (remoteOptions.retry = NETWORK_RETRY); (_b = remoteOptions.retryInterval) !== null && _b !== void 0 ? _b : (remoteOptions.retryInterval = NETWORK_RETRY_INTERVAL); for (let i = 0; i < remoteOptions.retry + 1; i++) { // default reflog message is 'fetch' // https://libgit2.org/libgit2/#HEAD/group/remote/git_remote_fetch // @ts-ignore // eslint-disable-next-line no-await-in-loop const res = await remote .fetch([ `+refs/heads/${localBranchName}:refs/remotes/${remoteName}/${remoteBranchName}`, ], { callbacks }, 'Fetch from ' + remoteName) .catch((err) => err); // remote must be disconnected after remote.fetch() // eslint-disable-next-line no-await-in-loop await remote.disconnect(); // It leaks memory if not cleanup repos.cleanup(); let error; if (typeof res !== 'number' && typeof res !== 'undefined') { error = res.message; } else { break; } // if (error !== 'undefined') console.warn('connect fetch error: ' + error); switch (true) { case /^remote '.+?' does not exist/.test(error): throw new git_documentdb_remote_errors_1.InvalidGitRemoteError(error); case error.startsWith('unsupported URL protocol'): case error.startsWith('malformed URL'): throw new git_documentdb_remote_errors_1.InvalidURLFormatError(error); // NodeGit throws them when network is limited. case error.startsWith('failed to send request'): case error.startsWith('failed to resolve address'): case error.startsWith('failed to connect'): // Ubuntu if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.NetworkError(error); } break; case error.startsWith('unexpected HTTP status code: 401'): // 401 on Ubuntu case error.startsWith('request failed with status code: 401'): // 401 on Windows case error.startsWith('remote authentication required but no callback set'): // Ubuntu case error.startsWith('could not find appropriate mechanism for credentials'): // Ubuntu case error.startsWith('Method connect has thrown an error'): case error.startsWith('remote credential provider returned an invalid cred type'): // on Ubuntu case error.startsWith('Failed to retrieve list of SSH authentication methods'): case error.startsWith('too many redirects or authentication replays'): throw new git_documentdb_remote_errors_1.HTTPError401AuthorizationRequired(error); case error.startsWith('unexpected http status code: 404'): // 404 on Ubuntu case error.startsWith('request failed with status code: 404'): // 404 on Windows throw new git_documentdb_remote_errors_1.HTTPError404NotFound(error); default: if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.CannotConnectError(error); } } // eslint-disable-next-line no-await-in-loop await sleep(remoteOptions.retryInterval); } } exports.fetch = fetch; /** * Calc distance * * @internal */ function calcDistance(baseCommitOid, localCommitOid, remoteCommitOid) { if (baseCommitOid === undefined) { return { ahead: undefined, behind: undefined, }; } return { ahead: localCommitOid !== baseCommitOid ? 1 : 0, behind: remoteCommitOid !== baseCommitOid ? 1 : 0, }; } /** * git push * * @throws {@link InvalidGitRemoteError} * @throws {@link UnfetchedCommitExistsError} * @throws {@link InvalidURLFormatError} * @throws {@link NetworkError} * @throws {@link HTTPError401AuthorizationRequired} * @throws {@link HTTPError404NotFound} * @throws {@link HTTPError403Forbidden} * @throws {@link CannotConnectError} * * @throws # Errors from validatePushResult * @throws - {@link UnfetchedCommitExistsError} * @throws - {@link CannotConnectError} * * @throws # Errors from createCredentialForGitHub * @throws - {@link InvalidURLFormatError} * @throws - {@link InvalidRepositoryURLError} * * @throws # Errors from createCredentialForSSH * @throws - {@link InvalidSSHKeyPathError} * * @throws # Errors from createCredential * @throws - {@link InvalidAuthenticationTypeError} * * @public */ // eslint-disable-next-line complexity async function push(workingDir, remoteOptions, remoteName = 'origin', localBranchName = 'main', remoteBranchName = 'main', logger) { var _a, _b; logger !== null && logger !== void 0 ? logger : (logger = new tslog_1.Logger({ name: 'plugin-nodegit', minLevel: 'trace', displayDateTime: false, displayFunctionName: false, displayFilePath: 'hidden', })); logger.debug(`remote-nodegit: push: ${remoteOptions.remoteUrl}`); const repos = await nodegit_1.default.Repository.open(workingDir); const remote = await repos .getRemote(remoteName) .catch((err) => { if (/^remote '.+?' does not exist/.test(err.message)) { throw new git_documentdb_remote_errors_1.InvalidGitRemoteError(err.message); } throw err; }); const callbacks = authentication_1.createCredentialCallback(remoteOptions); (_a = remoteOptions.retry) !== null && _a !== void 0 ? _a : (remoteOptions.retry = NETWORK_RETRY); (_b = remoteOptions.retryInterval) !== null && _b !== void 0 ? _b : (remoteOptions.retryInterval = NETWORK_RETRY_INTERVAL); for (let i = 0; i < remoteOptions.retry + 1; i++) { // eslint-disable-next-line no-await-in-loop const res = await remote .push([`refs/heads/${localBranchName}:refs/heads/${remoteBranchName}`], { callbacks, }) .catch((err) => { if (err.message.startsWith('cannot push because a reference that you are trying to update on the remote contains commits that are not present locally') || err.message.startsWith('cannot push non-fastforwardable reference')) { throw new git_documentdb_remote_errors_1.UnfetchedCommitExistsError(); } return err; }) .finally(() => { // It leaks memory if not cleanup repos.cleanup(); }); let error = ''; if (typeof res !== 'number' && typeof res !== 'undefined') { error = res.message; } else { break; } // console.warn('connect push error: ' + error); switch (true) { case error.startsWith('unsupported URL protocol'): case error.startsWith('malformed URL'): throw new git_documentdb_remote_errors_1.InvalidURLFormatError(error); // NodeGit throws them when network is limited. case error.startsWith('failed to send request'): case error.startsWith('failed to resolve address'): case error.startsWith('failed to connect'): // Ubuntu if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.NetworkError(error); } break; case error.startsWith('unexpected HTTP status code: 401'): // 401 on Ubuntu case error.startsWith('request failed with status code: 401'): // 401 on Windows case error.startsWith('remote authentication required but no callback set'): // Ubuntu case error.startsWith('could not find appropriate mechanism for credentials'): // Ubuntu case error.startsWith('Method connect has thrown an error'): case error.startsWith('remote credential provider returned an invalid cred type'): // on Ubuntu case error.startsWith('Failed to retrieve list of SSH authentication methods'): case error.startsWith('too many redirects or authentication replays'): throw new git_documentdb_remote_errors_1.HTTPError401AuthorizationRequired(error); case error.startsWith('unexpected http status code: 404'): // 404 on Ubuntu case error.startsWith('request failed with status code: 404'): // 404 on Windows throw new git_documentdb_remote_errors_1.HTTPError404NotFound(error); case error.startsWith('unexpected http status code: 403'): // 403 on Ubuntu case error.startsWith('request failed with status code: 403'): // 403 on Windows case error.startsWith('Error: ERROR: Permission to'): { throw new git_documentdb_remote_errors_1.HTTPError403Forbidden(error); } default: if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.CannotConnectError(error); } } // eslint-disable-next-line no-await-in-loop await sleep(remoteOptions.retryInterval); } await validatePushResult(repos, remote, remoteOptions, workingDir, remoteName, localBranchName, remoteBranchName, callbacks); } exports.push = push; /** * NodeGit.Remote.push does not throw following error in race condition: * 'cannot push because a reference that you are trying to update on the remote contains commits that are not present locally' * So check remote changes again. * * @throws {@link CannotConnectError} * @throws {@link UnfetchedCommitExistsError} * * @internal */ async function validatePushResult(repos, remote, remoteOptions, workingDir, remoteName, localBranchName, remoteBranchName, callbacks) { for (let i = 0; i < remoteOptions.retry + 1; i++) { // eslint-disable-next-line no-await-in-loop const error = await remote .fetch([ `+refs/heads/${localBranchName}:refs/remotes/${remoteName}/${remoteBranchName}`, ], { callbacks }, 'Fetch from ' + remoteName) // This is OK .catch((err) => { // push() already check errors except network errors. // So throw only network errors here. if (i >= remoteOptions.retry) { throw new git_documentdb_remote_errors_1.CannotConnectError(err.message); } else return err; }) .finally(async () => { // remote must be disconnected after remote.fetch() await remote.disconnect(); repos.cleanup(); }); if (!(error instanceof Error)) { break; } // eslint-disable-next-line no-await-in-loop await sleep(remoteOptions.retryInterval); } // Use isomorphic-git to avoid memory leak const localCommitOid = await isomorphic_git_1.default.resolveRef({ fs: fs_1.default, dir: workingDir, ref: 'HEAD', }); const remoteCommitOid = await isomorphic_git_1.default.resolveRef({ fs: fs_1.default, dir: workingDir, ref: `refs/remotes/${remoteName}/${remoteBranchName}`, }); const [baseCommitOid] = await isomorphic_git_1.default.findMergeBase({ fs: fs_1.default, dir: workingDir, oids: [localCommitOid, remoteCommitOid], }); const distance = await calcDistance(baseCommitOid, localCommitOid, remoteCommitOid); if (distance.behind && distance.behind > 0) { throw new git_documentdb_remote_errors_1.UnfetchedCommitExistsError(); } } //# sourceMappingURL=remote-nodegit.js.map