refactor(backend-common): deprecate isomorphic git class
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/plugin-scaffolder-node': patch
|
||||
---
|
||||
|
||||
Preparing for a stable new backend system release, we are deprecating utilities in the `backend-common` that are not used by the core framework, such as the isomorphic `Git` class. As we will no longer support the isomorphic `Git` utility in the framework packages, we recommend plugins that start maintaining their own implementation of this class.
|
||||
@@ -71,7 +71,7 @@ import { V1PodTemplateSpec } from '@kubernetes/client-node';
|
||||
import * as winston from 'winston';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type AuthCallbackOptions = {
|
||||
onAuth: AuthCallback;
|
||||
logger?: LoggerService;
|
||||
@@ -370,7 +370,7 @@ export function getRootLogger(): winston.Logger;
|
||||
// @public
|
||||
export function getVoidLogger(): winston.Logger;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export class Git {
|
||||
// (undocumented)
|
||||
add(options: { dir: string; filepath: string }): Promise<void>;
|
||||
@@ -818,7 +818,7 @@ export function setRootLogger(newLogger: winston.Logger): void;
|
||||
// @public @deprecated
|
||||
export const SingleHostDiscovery: typeof HostDiscovery_2;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type StaticAuthOptions = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './scm';
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import git, {
|
||||
ProgressCallback,
|
||||
MergeResult,
|
||||
ReadCommitResult,
|
||||
AuthCallback,
|
||||
} from 'isomorphic-git';
|
||||
import http from 'isomorphic-git/http/node';
|
||||
import fs from 'fs-extra';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
|
||||
function isAuthCallbackOptions(
|
||||
options: StaticAuthOptions | AuthCallbackOptions,
|
||||
): options is AuthCallbackOptions {
|
||||
return 'onAuth' in options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure static credential for authentication
|
||||
* @public
|
||||
* @deprecated This type is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493.
|
||||
*/
|
||||
export type StaticAuthOptions = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
logger?: LoggerService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure an authentication callback that can provide credentials on demand
|
||||
* @public
|
||||
* @deprecated This type is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493.
|
||||
*/
|
||||
export type AuthCallbackOptions = {
|
||||
onAuth: AuthCallback;
|
||||
logger?: LoggerService;
|
||||
};
|
||||
|
||||
/*
|
||||
provider username password
|
||||
Azure 'notempty' token
|
||||
Bitbucket Cloud 'x-token-auth' token
|
||||
Bitbucket Server username password or token
|
||||
GitHub 'x-access-token' token
|
||||
GitLab 'oauth2' token
|
||||
|
||||
From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub
|
||||
|
||||
Or token provided as `token` for Bearer auth header
|
||||
instead of Basic Auth (e.g., Bitbucket Server).
|
||||
*/
|
||||
|
||||
/**
|
||||
* A convenience wrapper around the `isomorphic-git` library.
|
||||
* @public
|
||||
* @deprecated This class is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493.
|
||||
*/
|
||||
|
||||
export class Git {
|
||||
private readonly headers: {
|
||||
[x: string]: string;
|
||||
};
|
||||
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
onAuth: AuthCallback;
|
||||
token?: string;
|
||||
logger?: LoggerService;
|
||||
},
|
||||
) {
|
||||
this.onAuth = config.onAuth;
|
||||
|
||||
this.headers = {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
...(config.token ? { Authorization: `Bearer ${config.token}` } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async add(options: { dir: string; filepath: string }): Promise<void> {
|
||||
const { dir, filepath } = options;
|
||||
this.config.logger?.info(`Adding file {dir=${dir},filepath=${filepath}}`);
|
||||
|
||||
return git.add({ fs, dir, filepath });
|
||||
}
|
||||
|
||||
async addRemote(options: {
|
||||
dir: string;
|
||||
remote: string;
|
||||
url: string;
|
||||
force?: boolean;
|
||||
}): Promise<void> {
|
||||
const { dir, url, remote, force } = options;
|
||||
this.config.logger?.info(
|
||||
`Creating new remote {dir=${dir},remote=${remote},url=${url}}`,
|
||||
);
|
||||
return git.addRemote({ fs, dir, remote, url, force });
|
||||
}
|
||||
|
||||
async deleteRemote(options: { dir: string; remote: string }): Promise<void> {
|
||||
const { dir, remote } = options;
|
||||
this.config.logger?.info(`Deleting remote {dir=${dir},remote=${remote}}`);
|
||||
return git.deleteRemote({ fs, dir, remote });
|
||||
}
|
||||
|
||||
async checkout(options: { dir: string; ref: string }): Promise<void> {
|
||||
const { dir, ref } = options;
|
||||
this.config.logger?.info(`Checking out branch {dir=${dir},ref=${ref}}`);
|
||||
|
||||
return git.checkout({ fs, dir, ref });
|
||||
}
|
||||
|
||||
async branch(options: { dir: string; ref: string }): Promise<void> {
|
||||
const { dir, ref } = options;
|
||||
this.config.logger?.info(`Creating branch {dir=${dir},ref=${ref}`);
|
||||
|
||||
return git.branch({ fs, dir, ref });
|
||||
}
|
||||
|
||||
async commit(options: {
|
||||
dir: string;
|
||||
message: string;
|
||||
author: { name: string; email: string };
|
||||
committer: { name: string; email: string };
|
||||
}): Promise<string> {
|
||||
const { dir, message, author, committer } = options;
|
||||
this.config.logger?.info(
|
||||
`Committing file to repo {dir=${dir},message=${message}}`,
|
||||
);
|
||||
return git.commit({ fs, dir, message, author, committer });
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/clone */
|
||||
async clone(options: {
|
||||
url: string;
|
||||
dir: string;
|
||||
ref?: string;
|
||||
depth?: number;
|
||||
noCheckout?: boolean;
|
||||
}): Promise<void> {
|
||||
const { url, dir, ref, depth, noCheckout } = options;
|
||||
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
|
||||
|
||||
try {
|
||||
return await git.clone({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
ref,
|
||||
singleBranch: true,
|
||||
depth: depth ?? 1,
|
||||
noCheckout,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: this.headers,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
} catch (ex) {
|
||||
this.config.logger?.error(`Failed to clone repo {dir=${dir},url=${url}}`);
|
||||
if (ex.data) {
|
||||
throw new Error(`${ex.message} {data=${JSON.stringify(ex.data)}}`);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/currentBranch */
|
||||
async currentBranch(options: {
|
||||
dir: string;
|
||||
fullName?: boolean;
|
||||
}): Promise<string | undefined> {
|
||||
const { dir, fullName = false } = options;
|
||||
return git.currentBranch({ fs, dir, fullname: fullName }) as Promise<
|
||||
string | undefined
|
||||
>;
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/fetch */
|
||||
async fetch(options: {
|
||||
dir: string;
|
||||
remote?: string;
|
||||
tags?: boolean;
|
||||
}): Promise<void> {
|
||||
const { dir, remote = 'origin', tags = false } = options;
|
||||
this.config.logger?.info(
|
||||
`Fetching remote=${remote} for repository {dir=${dir}}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await git.fetch({
|
||||
fs,
|
||||
http,
|
||||
dir,
|
||||
remote,
|
||||
tags,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: this.headers,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
} catch (ex) {
|
||||
this.config.logger?.error(
|
||||
`Failed to fetch repo {dir=${dir},remote=${remote}}`,
|
||||
);
|
||||
if (ex.data) {
|
||||
throw new Error(`${ex.message} {data=${JSON.stringify(ex.data)}}`);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async init(options: { dir: string; defaultBranch?: string }): Promise<void> {
|
||||
const { dir, defaultBranch = 'master' } = options;
|
||||
this.config.logger?.info(`Init git repository {dir=${dir}}`);
|
||||
|
||||
return git.init({
|
||||
fs,
|
||||
dir,
|
||||
defaultBranch,
|
||||
});
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/merge */
|
||||
async merge(options: {
|
||||
dir: string;
|
||||
theirs: string;
|
||||
ours?: string;
|
||||
author: { name: string; email: string };
|
||||
committer: { name: string; email: string };
|
||||
}): Promise<MergeResult> {
|
||||
const { dir, theirs, ours, author, committer } = options;
|
||||
this.config.logger?.info(
|
||||
`Merging branch '${theirs}' into '${ours}' for repository {dir=${dir}}`,
|
||||
);
|
||||
|
||||
// If ours is undefined, current branch is used.
|
||||
return git.merge({
|
||||
fs,
|
||||
dir,
|
||||
ours,
|
||||
theirs,
|
||||
author,
|
||||
committer,
|
||||
});
|
||||
}
|
||||
|
||||
async push(options: {
|
||||
dir: string;
|
||||
remote: string;
|
||||
remoteRef?: string;
|
||||
force?: boolean;
|
||||
}) {
|
||||
const { dir, remote, remoteRef, force } = options;
|
||||
this.config.logger?.info(
|
||||
`Pushing directory to remote {dir=${dir},remote=${remote}}`,
|
||||
);
|
||||
try {
|
||||
return await git.push({
|
||||
fs,
|
||||
dir,
|
||||
http,
|
||||
onProgress: this.onProgressHandler(),
|
||||
remoteRef,
|
||||
force,
|
||||
headers: this.headers,
|
||||
remote,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
} catch (ex) {
|
||||
this.config.logger?.error(
|
||||
`Failed to push to repo {dir=${dir}, remote=${remote}}`,
|
||||
);
|
||||
if (ex.data) {
|
||||
throw new Error(`${ex.message} {data=${JSON.stringify(ex.data)}}`);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/readCommit */
|
||||
async readCommit(options: {
|
||||
dir: string;
|
||||
sha: string;
|
||||
}): Promise<ReadCommitResult> {
|
||||
const { dir, sha } = options;
|
||||
return git.readCommit({ fs, dir, oid: sha });
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/remove */
|
||||
async remove(options: { dir: string; filepath: string }): Promise<void> {
|
||||
const { dir, filepath } = options;
|
||||
this.config.logger?.info(
|
||||
`Removing file from git index {dir=${dir},filepath=${filepath}}`,
|
||||
);
|
||||
return git.remove({ fs, dir, filepath });
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/resolveRef */
|
||||
async resolveRef(options: { dir: string; ref: string }): Promise<string> {
|
||||
const { dir, ref } = options;
|
||||
return git.resolveRef({ fs, dir, ref });
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/log */
|
||||
async log(options: {
|
||||
dir: string;
|
||||
ref?: string;
|
||||
}): Promise<ReadCommitResult[]> {
|
||||
const { dir, ref } = options;
|
||||
return git.log({
|
||||
fs,
|
||||
dir,
|
||||
ref: ref ?? 'HEAD',
|
||||
});
|
||||
}
|
||||
|
||||
private onAuth: AuthCallback;
|
||||
|
||||
private onProgressHandler = (): ProgressCallback => {
|
||||
let currentPhase = '';
|
||||
|
||||
return event => {
|
||||
if (currentPhase !== event.phase) {
|
||||
currentPhase = event.phase;
|
||||
this.config.logger?.info(event.phase);
|
||||
}
|
||||
const total = event.total
|
||||
? `${Math.round((event.loaded / event.total) * 100)}%`
|
||||
: event.loaded;
|
||||
this.config.logger?.debug(`status={${event.phase},total={${total}}}`);
|
||||
};
|
||||
};
|
||||
|
||||
static fromAuth = (options: StaticAuthOptions | AuthCallbackOptions) => {
|
||||
if (isAuthCallbackOptions(options)) {
|
||||
const { onAuth, logger } = options;
|
||||
return new Git({ onAuth, logger });
|
||||
}
|
||||
|
||||
const { username, password, token, logger } = options;
|
||||
return new Git({ onAuth: () => ({ username, password }), token, logger });
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
export { legacyPlugin, makeLegacyPlugin } from './legacy';
|
||||
export type { LegacyCreateRouter } from './legacy';
|
||||
export { Git } from './deprecated';
|
||||
export type { StaticAuthOptions, AuthCallbackOptions } from './deprecated';
|
||||
export * from './auth';
|
||||
export * from './cache';
|
||||
export { loadBackendConfig } from './config';
|
||||
@@ -32,7 +34,6 @@ export * from './logging';
|
||||
export * from './middleware';
|
||||
export * from './paths';
|
||||
export * from './reading';
|
||||
export * from './scm';
|
||||
export * from './service';
|
||||
export * from './tokens';
|
||||
export * from './util';
|
||||
|
||||
@@ -47,7 +47,7 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
});
|
||||
|
||||
const cloneMock = jest.fn(() => Promise.resolve());
|
||||
jest.mock('../scm', () => ({
|
||||
jest.mock('./git', () => ({
|
||||
Git: {
|
||||
fromAuth: () => ({
|
||||
clone: cloneMock,
|
||||
|
||||
@@ -14,7 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import { Base64Decode } from 'base64-stream';
|
||||
import concatStream from 'concat-stream';
|
||||
import fs from 'fs-extra';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import os from 'os';
|
||||
import { join as joinPath } from 'path';
|
||||
import { Readable, pipeline as pipelineCb } from 'stream';
|
||||
import tar from 'tar';
|
||||
import { promisify } from 'util';
|
||||
import {
|
||||
GerritIntegration,
|
||||
ScmIntegrations,
|
||||
@@ -26,16 +34,7 @@ import {
|
||||
parseGerritGitilesUrl,
|
||||
parseGerritJsonResponse,
|
||||
} from '@backstage/integration';
|
||||
import { Base64Decode } from 'base64-stream';
|
||||
import concatStream from 'concat-stream';
|
||||
import fs from 'fs-extra';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import os from 'os';
|
||||
import { join as joinPath } from 'path';
|
||||
import { Readable, pipeline as pipelineCb } from 'stream';
|
||||
import tar from 'tar';
|
||||
import { promisify } from 'util';
|
||||
import { Git } from '../scm';
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import {
|
||||
ReadTreeOptions,
|
||||
ReadTreeResponse,
|
||||
@@ -46,6 +45,13 @@ import {
|
||||
SearchResponse,
|
||||
UrlReader,
|
||||
} from './types';
|
||||
import { StaticAuthOptions, AuthCallbackOptions, Git } from './git';
|
||||
|
||||
export function isAuthCallbackOptions(
|
||||
options: StaticAuthOptions | AuthCallbackOptions,
|
||||
): options is AuthCallbackOptions {
|
||||
return 'onAuth' in options;
|
||||
}
|
||||
|
||||
const pipeline = promisify(pipelineCb);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
jest.mock('../scm', () => ({
|
||||
jest.mock('./git', () => ({
|
||||
Git: {
|
||||
fromAuth: () => ({
|
||||
clone: jest.fn(() => Promise.resolve({})),
|
||||
|
||||
@@ -30,7 +30,7 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
jest.mock('../scm', () => ({
|
||||
jest.mock('./git', () => ({
|
||||
Git: {
|
||||
fromAuth: () => ({
|
||||
clone: jest.fn(() => Promise.resolve({})),
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import isomorphicGit, { ProgressCallback, AuthCallback } from 'isomorphic-git';
|
||||
import http from 'isomorphic-git/http/node';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { isAuthCallbackOptions } from './GerritUrlReader';
|
||||
|
||||
/**
|
||||
* Configure static credential for authentication
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type StaticAuthOptions = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
logger?: LoggerService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure an authentication callback that can provide credentials on demand
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AuthCallbackOptions = {
|
||||
onAuth: AuthCallback;
|
||||
logger?: LoggerService;
|
||||
};
|
||||
|
||||
/*
|
||||
provider username password
|
||||
Azure 'notempty' token
|
||||
Bitbucket Cloud 'x-token-auth' token
|
||||
Bitbucket Server username password or token
|
||||
GitHub 'x-access-token' token
|
||||
GitLab 'oauth2' token
|
||||
|
||||
From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub
|
||||
|
||||
Or token provided as `token` for Bearer auth header
|
||||
instead of Basic Auth (e.g., Bitbucket Server).
|
||||
*/
|
||||
/**
|
||||
* A convenience wrapper around the `isomorphic-git` library.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Git {
|
||||
private readonly headers: {
|
||||
[x: string]: string;
|
||||
};
|
||||
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
onAuth: AuthCallback;
|
||||
token?: string;
|
||||
logger?: LoggerService;
|
||||
},
|
||||
) {
|
||||
this.onAuth = config.onAuth;
|
||||
|
||||
this.headers = {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
...(config.token ? { Authorization: `Bearer ${config.token}` } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** https://isomorphic-git.org/docs/en/clone */
|
||||
async clone(options: {
|
||||
url: string;
|
||||
dir: string;
|
||||
ref?: string;
|
||||
depth?: number;
|
||||
noCheckout?: boolean;
|
||||
}): Promise<void> {
|
||||
const { url, dir, ref, depth, noCheckout } = options;
|
||||
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
|
||||
|
||||
try {
|
||||
return await isomorphicGit.clone({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
ref,
|
||||
singleBranch: true,
|
||||
depth: depth ?? 1,
|
||||
noCheckout,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: this.headers,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
} catch (ex) {
|
||||
this.config.logger?.error(`Failed to clone repo {dir=${dir},url=${url}}`);
|
||||
if (ex.data) {
|
||||
throw new Error(`${ex.message} {data=${JSON.stringify(ex.data)}}`);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private onAuth: AuthCallback;
|
||||
|
||||
private onProgressHandler = (): ProgressCallback => {
|
||||
let currentPhase = '';
|
||||
|
||||
return event => {
|
||||
if (currentPhase !== event.phase) {
|
||||
currentPhase = event.phase;
|
||||
this.config.logger?.info(event.phase);
|
||||
}
|
||||
const total = event.total
|
||||
? `${Math.round((event.loaded / event.total) * 100)}%`
|
||||
: event.loaded;
|
||||
this.config.logger?.debug(`status={${event.phase},total={${total}}}`);
|
||||
};
|
||||
};
|
||||
|
||||
static fromAuth = (options: StaticAuthOptions | AuthCallbackOptions) => {
|
||||
if (isAuthCallbackOptions(options)) {
|
||||
const { onAuth, logger } = options;
|
||||
return new Git({ onAuth, logger });
|
||||
}
|
||||
|
||||
const { username, password, token, logger } = options;
|
||||
return new Git({ onAuth: () => ({ username, password }), token, logger });
|
||||
};
|
||||
}
|
||||
@@ -54,6 +54,7 @@
|
||||
"@backstage/types": "workspace:^",
|
||||
"fs-extra": "^11.2.0",
|
||||
"globby": "^11.0.0",
|
||||
"isomorphic-git": "^1.23.0",
|
||||
"jsonschema": "^1.2.6",
|
||||
"p-limit": "^3.1.0",
|
||||
"winston": "^3.2.1",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Git, getVoidLogger } from '@backstage/backend-common';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { Git } from '../scm';
|
||||
import {
|
||||
commitAndPushRepo,
|
||||
initRepoAndPush,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
cloneRepo,
|
||||
} from './gitHelpers';
|
||||
|
||||
jest.mock('@backstage/backend-common', () => ({
|
||||
jest.mock('../scm', () => ({
|
||||
Git: {
|
||||
fromAuth: jest.fn().mockReturnValue({
|
||||
init: jest.fn(),
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Git } from '@backstage/backend-common';
|
||||
import { Logger } from 'winston';
|
||||
import { Git } from '../scm';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
jest.mock('isomorphic-git');
|
||||
jest.mock('isomorphic-git/http/node');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
import * as isomorphic from 'isomorphic-git';
|
||||
import { Git } from './git';
|
||||
import http from 'isomorphic-git/http/node';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
describe('Git', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
it('should call isomorphic-git add with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const filepath = 'mockfile/path';
|
||||
|
||||
await git.add({ dir, filepath });
|
||||
|
||||
expect(isomorphic.add).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
filepath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRemote', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const remote = 'origin';
|
||||
const url = 'git@github.com/something/sads';
|
||||
const force = true;
|
||||
|
||||
await git.addRemote({ dir, remote, url, force });
|
||||
|
||||
expect(isomorphic.addRemote).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
remote,
|
||||
url,
|
||||
force,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should call isomorphic-git remove with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const filepath = 'mockfile/path';
|
||||
|
||||
await git.remove({ dir, filepath });
|
||||
|
||||
expect(isomorphic.remove).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
filepath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteRemote', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const remote = 'origin';
|
||||
|
||||
await git.deleteRemote({ dir, remote });
|
||||
|
||||
expect(isomorphic.deleteRemote).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
remote,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkout', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const ref = 'master';
|
||||
|
||||
await git.checkout({ dir, ref });
|
||||
|
||||
expect(isomorphic.checkout).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ref,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('branch', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const ref = 'master';
|
||||
|
||||
await git.branch({ dir, ref });
|
||||
|
||||
expect(isomorphic.branch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ref,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const message = 'Inital Commit';
|
||||
const author = {
|
||||
name: 'author',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
const committer = {
|
||||
name: 'comitter',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
|
||||
await git.commit({ dir, message, author, committer });
|
||||
|
||||
expect(isomorphic.commit).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
message,
|
||||
author,
|
||||
committer,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clone', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.clone({ url, dir });
|
||||
|
||||
expect(isomorphic.clone).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with the correct arguments (Bearer)', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
token: 'test',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.clone({ url, dir });
|
||||
|
||||
expect(isomorphic.clone).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
Authorization: 'Bearer test',
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass a function that returns the authorization as the onAuth handler when username and password are specified', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.clone({ url, dir });
|
||||
|
||||
const { onAuth } = (
|
||||
isomorphic.clone as unknown as jest.Mock<(typeof isomorphic)['clone']>
|
||||
).mock.calls[0][0]!;
|
||||
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
|
||||
it('should pass the provided callback as the onAuth handler when on auth is specified', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'from',
|
||||
password: 'callback',
|
||||
};
|
||||
|
||||
const git = Git.fromAuth({ onAuth: () => auth });
|
||||
|
||||
await git.clone({ url, dir });
|
||||
|
||||
const { onAuth } = (
|
||||
isomorphic.clone as unknown as jest.Mock<(typeof isomorphic)['clone']>
|
||||
).mock.calls[0][0]!;
|
||||
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
|
||||
it('should propagate the data from the error handler', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
(isomorphic.clone as jest.Mock).mockImplementation(() => {
|
||||
const error: Error & { data?: unknown } = new Error('mock error');
|
||||
error.data = { some: 'more information here' };
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(git.clone({ url, dir })).rejects.toThrow(
|
||||
'more information here',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentBranch', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const fullName = true;
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.currentBranch({ dir, fullName });
|
||||
|
||||
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
fullname: true,
|
||||
});
|
||||
|
||||
await git.currentBranch({ dir });
|
||||
|
||||
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
fullname: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.fetch({ remote, dir, tags: true });
|
||||
|
||||
expect(isomorphic.fetch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
tags: true,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with the correct arguments (Bearer)', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
token: 'test',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.fetch({ remote, dir });
|
||||
|
||||
expect(isomorphic.fetch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
tags: false,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
Authorization: 'Bearer test',
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.fetch({ remote, dir });
|
||||
|
||||
const { onAuth } = (
|
||||
isomorphic.fetch as unknown as jest.Mock<(typeof isomorphic)['fetch']>
|
||||
).mock.calls[0][0]!;
|
||||
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
|
||||
it('should propagate the data from the error handler', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
(isomorphic.fetch as jest.Mock).mockImplementation(() => {
|
||||
const error: Error & { data?: unknown } = new Error('mock error');
|
||||
error.data = { some: 'more information here' };
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(git.fetch({ remote, dir })).rejects.toThrow(
|
||||
'more information here',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const defaultBranch = 'master';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.init({ dir, defaultBranch });
|
||||
|
||||
expect(isomorphic.init).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
defaultBranch,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const author = {
|
||||
name: 'author',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
const committer = {
|
||||
name: 'comitter',
|
||||
email: 'test@backstage.io',
|
||||
};
|
||||
const theirs = 'master';
|
||||
const ours = 'production';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.merge({ dir, theirs, ours, author, committer });
|
||||
|
||||
expect(isomorphic.merge).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ours,
|
||||
theirs,
|
||||
author,
|
||||
committer,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
const remoteRef = 'master';
|
||||
const force = true;
|
||||
|
||||
await git.push({ dir, remote, remoteRef, force });
|
||||
|
||||
expect(isomorphic.push).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
remoteRef,
|
||||
force,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with the correct arguments (Bearer)', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
token: 'test',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
const remoteRef = 'master';
|
||||
const force = true;
|
||||
|
||||
await git.push({ dir, remote, remoteRef, force });
|
||||
|
||||
expect(isomorphic.push).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
remoteRef,
|
||||
force,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
Authorization: 'Bearer test',
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with remoteRef parameter', async () => {
|
||||
const remote = 'origin';
|
||||
const remoteRef = 'refs/for/master';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.push({ dir, remote, remoteRef });
|
||||
|
||||
expect(isomorphic.push).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
remoteRef,
|
||||
dir,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
const remoteRef = 'master';
|
||||
const force = true;
|
||||
|
||||
await git.push({ remote, dir, remoteRef, force });
|
||||
|
||||
const { onAuth } = (
|
||||
isomorphic.push as unknown as jest.Mock<(typeof isomorphic)['push']>
|
||||
).mock.calls[0][0]!;
|
||||
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
|
||||
it('should propagate the data from the error handler', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
const remoteRef = 'master';
|
||||
const force = true;
|
||||
|
||||
(isomorphic.push as jest.Mock).mockImplementation(() => {
|
||||
const error: Error & { data?: unknown } = new Error('mock error');
|
||||
error.data = { some: 'more information here' };
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(git.push({ remote, dir, remoteRef, force })).rejects.toThrow(
|
||||
'more information here',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readCommit', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const sha = 'as43bd7';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.readCommit({ dir, sha });
|
||||
|
||||
expect(isomorphic.readCommit).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
oid: sha,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRef', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const ref = 'as43bd7';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.resolveRef({ dir, ref });
|
||||
|
||||
expect(isomorphic.resolveRef).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ref,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('log', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const dir = '/some/mock/dir';
|
||||
const ref = 'as43bd7';
|
||||
|
||||
const git = Git.fromAuth({});
|
||||
|
||||
await git.log({ dir, ref });
|
||||
|
||||
expect(isomorphic.log).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ref,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Git } from './git';
|
||||
export type { StaticAuthOptions, AuthCallbackOptions } from './git';
|
||||
Reference in New Issue
Block a user