git-documentdb-plugin-remote-nodegit
Version:
GitDocumentDB plugin for remote connection using NodeGit
514 lines • 23.5 kB
JavaScript
"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