refactor(backend-common): deprecate isomorphic git class

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-05-03 09:25:34 +02:00
parent 129e0b9a49
commit 57f692e66c
19 changed files with 1178 additions and 21 deletions
+6
View File
@@ -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.
+3 -3
View File
@@ -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 });
};
}
+2 -1
View File
@@ -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({})),
+143
View File
@@ -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 });
};
}
+1
View File
@@ -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
+606
View File
@@ -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,
});
});
});
});
+18
View File
@@ -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';
+1
View File
@@ -6909,6 +6909,7 @@ __metadata:
"@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