diff --git a/.changeset/kind-buses-change.md b/.changeset/kind-buses-change.md new file mode 100644 index 0000000000..1d93e27dff --- /dev/null +++ b/.changeset/kind-buses-change.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-common': patch +'@backstage/techdocs-common': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Moving the Git actions to isomorphic-git instead of the node binding version of nodegit diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index b4be37c057..e2b7f36067 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -134,6 +134,7 @@ neuro newrelic nginx Niklas +nodegit nohoist nonces npm diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index e65edba0a5..a6b47bd7b4 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -45,6 +45,7 @@ "fs-extra": "^9.0.1", "git-url-parse": "^11.4.3", "helmet": "^4.0.0", + "isomorphic-git": "^1.8.0", "knex": "^0.21.6", "lodash": "^4.17.15", "logform": "^2.1.1", diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index e968edef69..3f04f32b34 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -24,3 +24,4 @@ export * from './reading'; export * from './service'; export * from './paths'; export * from './hot'; +export * from './scm'; diff --git a/packages/backend-common/src/scm/git.test.ts b/packages/backend-common/src/scm/git.test.ts new file mode 100644 index 0000000000..9af080a782 --- /dev/null +++ b/packages/backend-common/src/scm/git.test.ts @@ -0,0 +1,321 @@ +/* + * Copyright 2020 Spotify AB + * + * 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'; + + await git.addRemote({ dir, remote, url }); + + expect(isomorphic.addRemote).toHaveBeenCalledWith({ + fs, + dir, + remote, + url, + }); + }); + }); + + 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 pass a function that returns the authorization as the onAuth 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); + + 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); + }); + }); + + 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 }); + + expect(isomorphic.fetch).toHaveBeenCalledWith({ + fs, + http, + remote, + 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 = '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); + }); + }); + + describe('init', () => { + it('should call isomorphic-git with the correct arguments', async () => { + const dir = '/some/mock/dir'; + + const git = Git.fromAuth({}); + + await git.init({ dir }); + + expect(isomorphic.init).toHaveBeenCalledWith({ + fs, + dir, + }); + }); + }); + + 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); + + await git.push({ dir, remote }); + + expect(isomorphic.push).toHaveBeenCalledWith({ + fs, + http, + remote, + 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); + + await git.push({ remote, dir }); + + const { onAuth } = ((isomorphic.push as unknown) as jest.Mock< + typeof isomorphic['push'] + >).mock.calls[0][0]!; + + expect(onAuth()).toEqual(auth); + }); + }); + + 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, + }); + }); + }); +}); diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts new file mode 100644 index 0000000000..b0a1df6541 --- /dev/null +++ b/packages/backend-common/src/scm/git.ts @@ -0,0 +1,251 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, +} from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import fs from 'fs-extra'; +import { Logger } from 'winston'; + +/* +provider username password +GitHub token 'x-oauth-basic' +GitHub App token 'x-access-token' +BitBucket 'x-token-auth' token +GitLab 'oauth2' token +From : https://isomorphic-git.org/docs/en/onAuth + +Azure 'notempty' token +*/ +export class Git { + private constructor( + private readonly config: { + username?: string; + password?: string; + logger?: Logger; + }, + ) {} + + async add({ + dir, + filepath, + }: { + dir: string; + filepath: string; + }): Promise { + this.config.logger?.info(`Adding file {dir=${dir},filepath=${filepath}}`); + + return git.add({ fs, dir, filepath }); + } + + async addRemote({ + dir, + url, + remote, + }: { + dir: string; + remote: string; + url: string; + }): Promise { + this.config.logger?.info( + `Creating new remote {dir=${dir},remote=${remote},url=${url}}`, + ); + return git.addRemote({ fs, dir, remote, url }); + } + + async commit({ + dir, + message, + author, + committer, + }: { + dir: string; + message: string; + author: { name: string; email: string }; + committer: { name: string; email: string }; + }): Promise { + this.config.logger?.info( + `Committing file to repo {dir=${dir},message=${message}}`, + ); + + return git.commit({ fs, dir, message, author, committer }); + } + + async clone({ url, dir }: { url: string; dir: string }): Promise { + this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`); + return git.clone({ + fs, + http, + url, + dir, + singleBranch: true, + depth: 1, + onProgress: this.onProgressHandler(), + headers: { + 'user-agent': 'git/@isomorphic-git', + }, + onAuth: this.onAuth, + }); + } + + // https://isomorphic-git.org/docs/en/currentBranch + async currentBranch({ + dir, + fullName, + }: { + dir: string; + fullName?: boolean; + }): Promise { + const fullname = fullName ?? false; + return git.currentBranch({ fs, dir, fullname }) as Promise< + string | undefined + >; + } + + // https://isomorphic-git.org/docs/en/fetch + async fetch({ + dir, + remote, + }: { + dir: string; + remote?: string; + }): Promise { + const remoteValue = remote ?? 'origin'; + this.config.logger?.info( + `Fetching remote=${remoteValue} for repository {dir=${dir}}`, + ); + await git.fetch({ + fs, + http, + dir, + remote: remoteValue, + onProgress: this.onProgressHandler(), + headers: { + 'user-agent': 'git/@isomorphic-git', + }, + onAuth: this.onAuth, + }); + } + + async init({ dir }: { dir: string }): Promise { + this.config.logger?.info(`Init git repository {dir=${dir}}`); + + return git.init({ + fs, + dir, + }); + } + + // https://isomorphic-git.org/docs/en/merge + async merge({ + dir, + theirs, + ours, + author, + committer, + }: { + dir: string; + theirs: string; + ours?: string; + author: { name: string; email: string }; + committer: { name: string; email: string }; + }): Promise { + 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({ dir, remote }: { dir: string; remote: string }) { + this.config.logger?.info( + `Pushing directory to remote {dir=${dir},remote=${remote}}`, + ); + return git.push({ + fs, + dir, + http, + onProgress: this.onProgressHandler(), + headers: { + 'user-agent': 'git/@isomorphic-git', + }, + remote: remote, + onAuth: this.onAuth, + }); + } + + // https://isomorphic-git.org/docs/en/readCommit + async readCommit({ + dir, + sha, + }: { + dir: string; + sha: string; + }): Promise { + return git.readCommit({ fs, dir, oid: sha }); + } + + // https://isomorphic-git.org/docs/en/resolveRef + async resolveRef({ + dir, + ref, + }: { + dir: string; + ref: string; + }): Promise { + return git.resolveRef({ fs, dir, ref }); + } + + private onAuth = () => ({ + username: this.config.username, + password: this.config.password, + }); + + 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 = ({ + username, + password, + logger, + }: { + username?: string; + password?: string; + logger?: Logger; + }) => new Git({ username, password, logger }); +} diff --git a/packages/backend-common/src/scm/index.ts b/packages/backend-common/src/scm/index.ts new file mode 100644 index 0000000000..e967fffb44 --- /dev/null +++ b/packages/backend-common/src/scm/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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'; diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index e7244c4a87..7fc88a5238 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -51,7 +51,6 @@ "js-yaml": "^3.14.0", "mime-types": "^2.1.27", "mock-fs": "^4.13.0", - "nodegit": "^0.27.0", "recursive-readdir": "^2.2.2", "winston": "^3.2.1" }, @@ -62,7 +61,6 @@ "@types/js-yaml": "^3.12.5", "@types/mime-types": "^2.1.0", "@types/mock-fs": "^4.13.0", - "@types/nodegit": "^0.26.12", "@types/recursive-readdir": "^2.2.0" }, "jest": { diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index ab1e3e615f..f70b0f3840 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -17,9 +17,8 @@ import os from 'os'; import path from 'path'; import parseGitUrl from 'git-url-parse'; -import NodeGit, { Clone, Repository } from 'nodegit'; import fs from 'fs-extra'; -import { InputError, UrlReader } from '@backstage/backend-common'; +import { InputError, UrlReader, Git } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { getDefaultBranch } from './default-branch'; @@ -27,10 +26,6 @@ import { getGitRepoType, getTokenForGitRepo } from './git-auth'; import { RemoteProtocol } from './stages/prepare/types'; import { Logger } from 'winston'; -// Enables core.longpaths on windows to prevent crashing when checking out repos with long foldernames and/or deep nesting -// @ts-ignore -NodeGit.Libgit2.opts(28, 1); - export type ParsedLocationAnnotation = { type: RemoteProtocol; target: string; @@ -128,17 +123,61 @@ export const checkoutGitRepository = async ( const repositoryTmpPath = await getGitRepositoryTempFolder(repoUrl, config); const token = await getTokenForGitRepo(repoUrl, config); + // Initialize a git client + let git = Git.fromAuth({ logger }); + + // Docs about why username and password are set to these specific values. + // https://isomorphic-git.org/docs/en/onAuth#oauth2-tokens + if (token) { + const type = getGitRepoType(repoUrl); + switch (type) { + case 'github': + git = Git.fromAuth({ + username: token, + password: 'x-oauth-basic', + logger, + }); + parsedGitLocation.token = `${token}:x-oauth-basic`; + break; + case 'gitlab': + git = Git.fromAuth({ + username: 'oauth2', + password: token, + logger, + }); + parsedGitLocation.token = `dummyUsername:${token}`; + parsedGitLocation.git_suffix = true; + break; + case 'azure/api': + git = Git.fromAuth({ + username: 'notempty', + password: token, + logger: logger, + }); + break; + default: + parsedGitLocation.token = `:${token}`; + } + } + + // Pull from repository if it has already been cloned. if (fs.existsSync(repositoryTmpPath)) { try { - const repository = await Repository.open(repositoryTmpPath); - const currentBranchName = ( - await repository.getCurrentBranch() - ).shorthand(); - await repository.fetch('origin'); - await repository.mergeBranches( - currentBranchName, - `origin/${currentBranchName}`, - ); + const currentBranchName = await git.currentBranch({ + dir: repositoryTmpPath, + }); + + await git.fetch({ dir: repositoryTmpPath, remote: 'origin' }); + await git.merge({ + dir: repositoryTmpPath, + theirs: `origin/${currentBranchName}`, + ours: currentBranchName || undefined, + author: { name: 'Backstage TechDocs', email: 'techdocs@backstage.io' }, + committer: { + name: 'Backstage TechDocs', + email: 'techdocs@backstage.io', + }, + }); return repositoryTmpPath; } catch (e) { logger.info( @@ -148,26 +187,10 @@ export const checkoutGitRepository = async ( } } - if (token) { - const type = getGitRepoType(repoUrl); - switch (type) { - case 'gitlab': - // Personal Access Token - parsedGitLocation.token = `dummyUsername:${token}`; - parsedGitLocation.git_suffix = true; - break; - case 'github': - parsedGitLocation.token = `${token}:x-oauth-basic`; - break; - default: - parsedGitLocation.token = `:${token}`; - } - } - const repositoryCheckoutUrl = parsedGitLocation.toString('https'); fs.mkdirSync(repositoryTmpPath, { recursive: true }); - await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath); + await git.clone({ url: repositoryCheckoutUrl, dir: repositoryTmpPath }); return repositoryTmpPath; }; @@ -183,10 +206,11 @@ export const getLastCommitTimestamp = async ( logger, ); - const repository = await Repository.open(repositoryLocation); - const commit = await repository.getReferenceCommit('HEAD'); + const git = Git.fromAuth({ logger }); + const sha = await git.resolveRef({ dir: repositoryLocation, ref: 'HEAD' }); + const commit = await git.readCommit({ dir: repositoryLocation, sha }); - return commit.date().getTime(); + return commit.commit.committer.timestamp; }; export const getDocFilesFromRepository = async ( diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index dcf48f68a8..1a443ceb7c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -42,6 +42,7 @@ "command-exists-promise": "^2.0.2", "compression": "^1.7.4", "cors": "^2.8.5", + "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -49,20 +50,18 @@ "git-url-parse": "^11.4.3", "globby": "^11.0.0", "helmet": "^4.0.0", + "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", "morgan": "^1.10.0", - "nodegit": "0.27.0", "uuid": "^8.2.0", "winston": "^3.2.1", - "yaml": "^1.10.0", - "cross-fetch": "^3.0.6" + "yaml": "^1.10.0" }, "devDependencies": { "@backstage/cli": "^0.4.3", "@octokit/types": "^5.4.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", - "@types/nodegit": "0.26.11", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "yaml": "^1.10.0" diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 4d19069b5e..39efd1c62a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -const mocks = { - Clone: { clone: jest.fn() }, - CheckoutOptions: jest.fn(() => {}), -}; -jest.doMock('nodegit', () => mocks); jest.doMock('fs-extra', () => ({ promises: { mkdtemp: jest.fn(dir => `${dir}-static`), @@ -30,10 +25,16 @@ import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { getVoidLogger } from '@backstage/backend-common'; +import { getVoidLogger, Git } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; describe('AzurePreparer', () => { + const mockGitClient = { + clone: jest.fn(), + }; + + jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); + let mockEntity: TemplateEntityV1alpha1; beforeEach(() => { jest.clearAllMocks(); @@ -77,18 +78,7 @@ describe('AzurePreparer', () => { }; }); - it('calls the clone command with the correct arguments for a repository', async () => { - const preparer = new AzurePreparer(new ConfigReader({})); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - expect.any(String), - {}, - ); - }); - - it('calls the clone command with the correct arguments if an access token is provided for a repository', async () => { + it('initializes git client with the correct arguments if an access token is provided for a repository', async () => { const preparer = new AzurePreparer( new ConfigReader({ scaffolder: { @@ -100,36 +90,44 @@ describe('AzurePreparer', () => { }, }), ); + const logger = getVoidLogger(); + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + username: 'notempty', + password: 'fake-token', + logger, + }); + }); + it('calls the clone command with the correct arguments for a repository', async () => { + const preparer = new AzurePreparer(new ConfigReader({})); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - expect.any(String), - { - fetchOpts: { - callbacks: { - credentials: expect.anything(), - }, - }, - }, - ); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: + 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', + dir: expect.any(String), + }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { const preparer = new AzurePreparer(new ConfigReader({})); delete mockEntity.spec.path; + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', - expect.any(String), - {}, - ); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: + 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', + dir: expect.any(String), + }); }); it('return the temp directory with the path to the folder if it is specified', async () => { const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; + const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), }); @@ -142,6 +140,7 @@ describe('AzurePreparer', () => { it('return the working directory with the path to the folder if it is specified', async () => { const preparer = new AzurePreparer(new ConfigReader({})); mockEntity.spec.path = './template/test/1/2/3'; + const response = await preparer.prepare(mockEntity, { logger: getVoidLogger(), workingDirectory: '/workDir', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index 333bf9116c..a38c5f4fdc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -18,10 +18,9 @@ import fs from 'fs-extra'; import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; -import { InputError } from '@backstage/backend-common'; +import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import GitUriParser from 'git-url-parse'; -import { Clone, Cred } from 'nodegit'; import { Config } from '@backstage/config'; export class AzurePreparer implements PreparerBase { @@ -38,6 +37,7 @@ export class AzurePreparer implements PreparerBase { ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); + const { logger } = opts; if (!['azure/api', 'url'].includes(protocol)) { throw new InputError( @@ -57,19 +57,20 @@ export class AzurePreparer implements PreparerBase { template.spec.path ?? '.', ); - const options = this.privateToken - ? { - fetchOpts: { - callbacks: { - credentials: () => - // Username can anything but the empty string according to: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat - Cred.userpassPlaintextNew('notempty', this.privateToken), - }, - }, - } - : {}; + // Username can be anything but the empty string according to: + // https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat + const git = this.privateToken + ? Git.fromAuth({ + password: this.privateToken, + username: 'notempty', + logger, + }) + : Git.fromAuth({ logger }); - await Clone.clone(repositoryCheckoutUrl, tempDir, options); + await git.clone({ + url: repositoryCheckoutUrl, + dir: tempDir, + }); return path.resolve(tempDir, templateDirectory); } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 86a870330e..aa040bfe58 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -const mocks = { - Clone: { clone: jest.fn() }, - CheckoutOptions: jest.fn(() => {}), -}; -jest.doMock('nodegit', () => mocks); jest.doMock('fs-extra', () => ({ promises: { mkdtemp: jest.fn(dir => `${dir}-static`), @@ -30,10 +25,16 @@ import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { getVoidLogger } from '@backstage/backend-common'; +import { getVoidLogger, Git } from '@backstage/backend-common'; describe('GitHubPreparer', () => { let mockEntity: TemplateEntityV1alpha1; + const mockGitClient = { + clone: jest.fn(), + }; + + jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); + beforeEach(() => { jest.clearAllMocks(); mockEntity = { @@ -77,28 +78,24 @@ describe('GitHubPreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { const preparer = new GithubPreparer(); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://github.com/benjdlambert/backstage-graphql-template', - expect.any(String), - { - checkoutBranch: 'master', - }, - ); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://github.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), + }); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { const preparer = new GithubPreparer(); delete mockEntity.spec.path; + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://github.com/benjdlambert/backstage-graphql-template', - expect.any(String), - { - checkoutBranch: 'master', - }, - ); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://github.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), + }); }); it('return the temp directory with the path to the folder if it is specified', async () => { @@ -128,19 +125,14 @@ describe('GitHubPreparer', () => { it('calls the clone command with the token when provided', async () => { const preparer = new GithubPreparer({ token: 'abc' }); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://github.com/benjdlambert/backstage-graphql-template', - expect.any(String), - { - checkoutBranch: 'master', - fetchOpts: { - callbacks: { - credentials: expect.any(Function), - }, - }, - }, - ); + const logger = getVoidLogger(); + + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'abc', + password: 'x-oauth-basic', + }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index e6b41907be..e7fa0c9b72 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -18,10 +18,9 @@ import fs from 'fs-extra'; import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; -import { InputError } from '@backstage/backend-common'; +import { InputError, Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import GitUriParser from 'git-url-parse'; -import { Clone, CloneOptions, Cred } from 'nodegit'; export class GithubPreparer implements PreparerBase { token?: string; @@ -36,7 +35,7 @@ export class GithubPreparer implements PreparerBase { ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); - const { token } = this; + const { logger } = opts; if (!['github', 'url'].includes(protocol)) { throw new InputError( @@ -56,25 +55,21 @@ export class GithubPreparer implements PreparerBase { template.spec.path ?? '.', ); - let cloneOptions: CloneOptions = { - checkoutBranch: parsedGitLocation.ref, - }; + const checkoutLocation = path.resolve(tempDir, templateDirectory); - if (token) { - cloneOptions = { - ...cloneOptions, - fetchOpts: { - callbacks: { - credentials() { - return Cred.userpassPlaintextNew(token, 'x-oauth-basic'); - }, - }, - }, - }; - } + const git = this.token + ? Git.fromAuth({ + username: this.token, + password: 'x-oauth-basic', + logger, + }) + : Git.fromAuth({ logger }); - await Clone.clone(repositoryCheckoutUrl, tempDir, cloneOptions); + await git.clone({ + url: repositoryCheckoutUrl, + dir: tempDir, + }); - return path.resolve(tempDir, templateDirectory); + return checkoutLocation; } } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 8712507026..8dad349a13 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -13,11 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const mocks = { - Clone: { clone: jest.fn() }, - CheckoutOptions: jest.fn(() => {}), -}; -jest.doMock('nodegit', () => mocks); jest.doMock('fs-extra', () => ({ promises: { mkdtemp: jest.fn(dir => `${dir}-static`), @@ -30,7 +25,7 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; +import { getVoidLogger, Git } from '@backstage/backend-common'; const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({ apiVersion: 'backstage.io/v1alpha1', @@ -72,6 +67,12 @@ const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({ describe('GitLabPreparer', () => { let mockEntity: TemplateEntityV1alpha1; + const mockGitClient = { + clone: jest.fn(), + }; + + jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any); + beforeEach(() => { jest.clearAllMocks(); }); @@ -80,13 +81,13 @@ describe('GitLabPreparer', () => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://gitlab.com/benjdlambert/backstage-graphql-template', - expect.any(String), - {}, - ); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), + }); }); it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository using the ${protocol} protocol`, async () => { @@ -103,19 +104,15 @@ describe('GitLabPreparer', () => { }), ); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://gitlab.com/benjdlambert/backstage-graphql-template', - expect.any(String), - { - fetchOpts: { - callbacks: { - credentials: expect.anything(), - }, - }, - }, - ); + const logger = getVoidLogger(); + + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'oauth2', + password: 'fake-token', + }); }); it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository using the ${protocol} protocol`, async () => { @@ -127,32 +124,28 @@ describe('GitLabPreparer', () => { }), ); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://gitlab.com/benjdlambert/backstage-graphql-template', - expect.any(String), - { - fetchOpts: { - callbacks: { - credentials: expect.anything(), - }, - }, - }, - ); + const logger = getVoidLogger(); + + await preparer.prepare(mockEntity, { logger }); + + expect(Git.fromAuth).toHaveBeenCalledWith({ + logger, + username: 'oauth2', + password: 'fake-token', + }); }); it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => { const preparer = new GitlabPreparer(new ConfigReader({})); mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); - expect(mocks.Clone.clone).toHaveBeenNthCalledWith( - 1, - 'https://gitlab.com/benjdlambert/backstage-graphql-template', - expect.any(String), - {}, - ); + + expect(mockGitClient.clone).toHaveBeenCalledWith({ + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + dir: expect.any(String), + }); }); it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index c2c2dcd3fd..6e169baa98 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InputError } from '@backstage/backend-common'; +import { InputError, Git } from '@backstage/backend-common'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -22,7 +22,6 @@ import { } from '@backstage/integration'; import fs from 'fs-extra'; import GitUriParser from 'git-url-parse'; -import { Clone, Cred } from 'nodegit'; import os from 'os'; import path from 'path'; import { parseLocationAnnotation } from '../helpers'; @@ -46,6 +45,7 @@ export class GitlabPreparer implements PreparerBase { opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); + const { logger } = opts; const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) { @@ -67,17 +67,18 @@ export class GitlabPreparer implements PreparerBase { ); const token = this.getToken(parsedGitLocation.resource); - const options = token - ? { - fetchOpts: { - callbacks: { - credentials: () => Cred.userpassPlaintextNew('oauth2', token), - }, - }, - } - : {}; + const git = token + ? Git.fromAuth({ + password: token, + username: 'oauth2', + logger, + }) + : Git.fromAuth({ logger }); - await Clone.clone(repositoryCheckoutUrl, tempDir, options); + await git.clone({ + url: repositoryCheckoutUrl, + dir: tempDir, + }); return path.resolve(tempDir, templateDirectory); } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts index 9ea7d2de5f..f1e0cd6175 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.test.ts @@ -13,17 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -jest.mock('nodegit'); -jest.mock('azure-devops-node-api/GitApi'); -jest.mock('azure-devops-node-api/interfaces/GitInterfaces'); -jest.mock('./helpers', () => ({ - pushToRemoteUserPass: jest.fn(), -})); +jest.mock('./helpers'); import { AzurePublisher } from './azure'; import { GitApi } from 'azure-devops-node-api/GitApi'; -import { pushToRemoteUserPass } from './helpers'; +import * as helpers from './helpers'; +import { getVoidLogger } from '@backstage/backend-common'; const { mockGitApi } = require('azure-devops-node-api/GitApi') as { mockGitApi: { @@ -33,6 +28,7 @@ const { mockGitApi } = require('azure-devops-node-api/GitApi') as { describe('Azure Publisher', () => { const publisher = new AzurePublisher(new GitApi('', []), 'fake-token'); + const logger = getVoidLogger(); beforeEach(() => { jest.clearAllMocks(); @@ -50,6 +46,7 @@ describe('Azure Publisher', () => { owner: 'bob', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ @@ -63,12 +60,12 @@ describe('Azure Publisher', () => { }, 'project', ); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'https://dev.azure.com/organization/project/_git/repo', - 'notempty', - 'fake-token', - ); + expect(helpers.initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + auth: { username: 'notempty', password: 'fake-token' }, + logger, + }); }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts index 1e962bf223..ffe6845f01 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/azure.ts @@ -17,9 +17,9 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { GitApi } from 'azure-devops-node-api/GitApi'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { pushToRemoteUserPass } from './helpers'; import { JsonValue } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; +import { initRepoAndPush } from './helpers'; export class AzurePublisher implements PublisherBase { private readonly client: GitApi; @@ -33,11 +33,21 @@ export class AzurePublisher implements PublisherBase { async publish({ values, directory, + logger, }: PublisherOptions): Promise { const remoteUrl = await this.createRemote(values); - await pushToRemoteUserPass(directory, remoteUrl, 'notempty', this.token); const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`; + await initRepoAndPush({ + dir: directory, + remoteUrl, + auth: { + username: 'notempty', + password: this.token, + }, + logger, + }); + return { remoteUrl, catalogInfoUrl }; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index cda64faf25..22f1cd3172 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -15,10 +15,7 @@ */ jest.mock('@octokit/rest'); -jest.mock('nodegit'); -jest.mock('./helpers', () => ({ - pushToRemoteUserPass: jest.fn(), -})); +jest.mock('./helpers'); import { Octokit } from '@octokit/rest'; import { @@ -27,7 +24,8 @@ import { UsersGetByUsernameResponseData, } from '@octokit/types'; import { GithubPublisher } from './github'; -import { pushToRemoteUserPass } from './helpers'; +import { initRepoAndPush } from './helpers'; +import { getVoidLogger } from '@backstage/backend-common'; const { mockGithubClient } = require('@octokit/rest') as { mockGithubClient: { @@ -38,6 +36,7 @@ const { mockGithubClient } = require('@octokit/rest') as { }; describe('GitHub Publisher', () => { + const logger = getVoidLogger(); beforeEach(() => { jest.clearAllMocks(); }); @@ -69,6 +68,7 @@ describe('GitHub Publisher', () => { access: 'blam/team', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ @@ -91,12 +91,12 @@ describe('GitHub Publisher', () => { repo: 'test', permission: 'admin', }); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'https://github.com/backstage/backstage.git', - 'abc', - 'x-oauth-basic', - ); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://github.com/backstage/backstage.git', + auth: { username: 'abc', password: 'x-oauth-basic' }, + logger, + }); }); it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => { @@ -118,6 +118,7 @@ describe('GitHub Publisher', () => { access: 'blam', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ @@ -132,12 +133,13 @@ describe('GitHub Publisher', () => { private: false, }); expect(mockGithubClient.repos.addCollaborator).not.toHaveBeenCalled(); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'https://github.com/backstage/backstage.git', - 'abc', - 'x-oauth-basic', - ); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://github.com/backstage/backstage.git', + auth: { username: 'abc', password: 'x-oauth-basic' }, + logger, + }); }); }); @@ -161,6 +163,7 @@ describe('GitHub Publisher', () => { description: 'description', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ @@ -181,12 +184,12 @@ describe('GitHub Publisher', () => { username: 'bob', permission: 'admin', }); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'https://github.com/backstage/backstage.git', - 'abc', - 'x-oauth-basic', - ); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://github.com/backstage/backstage.git', + auth: { username: 'abc', password: 'x-oauth-basic' }, + logger, + }); }); }); @@ -216,6 +219,7 @@ describe('GitHub Publisher', () => { owner: 'bob', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ @@ -229,12 +233,12 @@ describe('GitHub Publisher', () => { private: true, visibility: 'internal', }); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'https://github.com/backstage/backstage.git', - 'abc', - 'x-oauth-basic', - ); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://github.com/backstage/backstage.git', + auth: { username: 'abc', password: 'x-oauth-basic' }, + logger, + }); }); }); @@ -263,6 +267,7 @@ describe('GitHub Publisher', () => { owner: 'bob', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ @@ -276,12 +281,12 @@ describe('GitHub Publisher', () => { name: 'test', private: true, }); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'https://github.com/backstage/backstage.git', - 'abc', - 'x-oauth-basic', - ); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'https://github.com/backstage/backstage.git', + auth: { username: 'abc', password: 'x-oauth-basic' }, + logger, + }); }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 64976b8e41..39a588882f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -16,7 +16,7 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Octokit } from '@octokit/rest'; -import { pushToRemoteUserPass } from './helpers'; +import { initRepoAndPush } from './helpers'; import { JsonValue } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; @@ -46,14 +46,20 @@ export class GithubPublisher implements PublisherBase { async publish({ values, directory, + logger, }: PublisherOptions): Promise { const remoteUrl = await this.createRemote(values); - await pushToRemoteUserPass( - directory, + + await initRepoAndPush({ + dir: directory, remoteUrl, - this.token, - 'x-oauth-basic', - ); + auth: { + username: this.token, + password: 'x-oauth-basic', + }, + logger, + }); + const catalogInfoUrl = remoteUrl.replace( /\.git$/, '/blob/master/catalog-info.yaml', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts index a87fa6c6d9..2f33358829 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.test.ts @@ -14,16 +14,14 @@ * limitations under the License. */ -jest.mock('nodegit'); jest.mock('@gitbeaker/node'); -jest.mock('./helpers', () => ({ - pushToRemoteUserPass: jest.fn(), -})); +jest.mock('./helpers'); import { GitlabPublisher } from './gitlab'; import { Gitlab as GitlabAPI } from '@gitbeaker/core'; import { Gitlab } from '@gitbeaker/node'; -import { pushToRemoteUserPass } from './helpers'; +import { initRepoAndPush } from './helpers'; +import { getVoidLogger } from '@backstage/backend-common'; const { mockGitlabClient } = require('@gitbeaker/node') as { mockGitlabClient: { @@ -34,6 +32,7 @@ const { mockGitlabClient } = require('@gitbeaker/node') as { }; describe('GitLab Publisher', () => { + const logger = getVoidLogger(); const publisher = new GitlabPublisher(new Gitlab({}), 'fake-token'); beforeEach(() => { @@ -56,6 +55,7 @@ describe('GitLab Publisher', () => { owner: 'bob', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ remoteUrl: 'mockclone' }); @@ -63,12 +63,12 @@ describe('GitLab Publisher', () => { namespace_id: 42, name: 'test', }); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'mockclone', - 'oauth2', - 'fake-token', - ); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'mockclone', + auth: { username: 'oauth2', password: 'fake-token' }, + logger, + }); }); it('should use gitbeaker to create a repo in the authed user if the namespace property is not set', async () => { @@ -86,6 +86,7 @@ describe('GitLab Publisher', () => { owner: 'bob', }, directory: '/tmp/test', + logger, }); expect(result).toEqual({ remoteUrl: 'mockclone' }); @@ -94,12 +95,12 @@ describe('GitLab Publisher', () => { namespace_id: 21, name: 'test', }); - expect(pushToRemoteUserPass).toHaveBeenCalledWith( - '/tmp/test', - 'mockclone', - 'oauth2', - 'fake-token', - ); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: '/tmp/test', + remoteUrl: 'mockclone', + auth: { username: 'oauth2', password: 'fake-token' }, + logger, + }); }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts index 4fae8e8a5c..156a4a249c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/gitlab.ts @@ -16,8 +16,8 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { Gitlab } from '@gitbeaker/core'; -import { pushToRemoteUserPass } from './helpers'; import { JsonValue } from '@backstage/config'; +import { initRepoAndPush } from './helpers'; import { RequiredTemplateValues } from '../templater'; export class GitlabPublisher implements PublisherBase { @@ -32,9 +32,19 @@ export class GitlabPublisher implements PublisherBase { async publish({ values, directory, + logger, }: PublisherOptions): Promise { const remoteUrl = await this.createRemote(values); - await pushToRemoteUserPass(directory, remoteUrl, 'oauth2', this.token); + + await initRepoAndPush({ + dir: directory, + remoteUrl, + auth: { + username: 'oauth2', + password: this.token, + }, + logger, + }); return { remoteUrl }; } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.test.ts deleted file mode 100644 index 3a317adacd..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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('nodegit'); -import * as NodeGit from 'nodegit'; -import { pushToRemoteCred } from './helpers'; - -const { - Repository, - mockRepo, - mockIndex, - Signature, - Remote, - mockRemote, - Cred, -} = require('nodegit') as { - Repository: jest.Mocked<{ init: any }>; - Signature: jest.Mocked<{ now: any }>; - Cred: jest.Mocked<{ userpassPlaintextNew: any }>; - Remote: jest.Mocked<{ create: any }>; - - mockIndex: jest.Mocked; - mockRepo: jest.Mocked; - mockRemote: jest.Mocked; -}; - -describe('pushToRemoteCred', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - const directory = '/tmp/test/dir'; - const remote = 'mockclone'; - const credentialsProvider = () => - NodeGit.Cred.userpassPlaintextNew('username', 'password'); - - it('should call init on the repo with the directory', async () => { - await pushToRemoteCred(directory, remote, credentialsProvider); - - expect(Repository.init).toHaveBeenCalledWith(directory, 0); - }); - - it('should call refresh index on the index and write the new files', async () => { - await pushToRemoteCred(directory, remote, credentialsProvider); - - expect(mockRepo.refreshIndex).toHaveBeenCalled(); - }); - - it('should call add all files and write', async () => { - await pushToRemoteCred(directory, remote, credentialsProvider); - - expect(mockIndex.addAll).toHaveBeenCalled(); - expect(mockIndex.write).toHaveBeenCalled(); - expect(mockIndex.writeTree).toHaveBeenCalled(); - }); - - it('should create a commit with on head with the right name and commiter', async () => { - const mockSignature = { mockSignature: 'bloblly' }; - Signature.now.mockReturnValue(mockSignature); - - await pushToRemoteCred(directory, remote, credentialsProvider); - - expect(Signature.now).toHaveBeenCalledTimes(2); - expect(Signature.now).toHaveBeenCalledWith( - 'Scaffolder', - 'scaffolder@backstage.io', - ); - - expect(mockRepo.createCommit).toHaveBeenCalledWith( - 'HEAD', - mockSignature, - mockSignature, - 'initial commit', - 'mockoid', - [], - ); - }); - - it('creates a remote with the repo and remote', async () => { - await pushToRemoteCred(directory, remote, credentialsProvider); - - expect(Remote.create).toHaveBeenCalledWith(mockRepo, 'origin', 'mockclone'); - }); - - it('shoud push to the remote repo', async () => { - await pushToRemoteCred(directory, remote, credentialsProvider); - - const [remotes, { callbacks }] = mockRemote.push.mock - .calls[0] as NodeGit.PushOptions[]; - - expect(remotes).toEqual(['refs/heads/master:refs/heads/master']); - - callbacks?.credentials?.(); - - expect(Cred.userpassPlaintextNew).toHaveBeenCalledWith( - 'username', - 'password', - ); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts index 51f5a4c854..796e48b8f8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/helpers.ts @@ -13,43 +13,57 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Repository, Remote, Signature, Cred } from 'nodegit'; -export async function pushToRemoteCred( - directory: string, - remote: string, - credentialsProvider: () => Cred, -): Promise { - const repo = await Repository.init(directory, 0); - const index = await repo.refreshIndex(); - await index.addAll(); - await index.write(); - const oid = await index.writeTree(); - await repo.createCommit( - 'HEAD', - Signature.now('Scaffolder', 'scaffolder@backstage.io'), - Signature.now('Scaffolder', 'scaffolder@backstage.io'), - 'initial commit', - oid, - [], - ); +import globby from 'globby'; +import { Logger } from 'winston'; +import { Git } from '@backstage/backend-common'; - const remoteRepo = await Remote.create(repo, 'origin', remote); +export async function initRepoAndPush({ + dir, + remoteUrl, + auth, + logger, +}: { + dir: string; + remoteUrl: string; + auth: { username: string; password: string }; + logger: Logger; +}): Promise { + const git = Git.fromAuth({ + username: auth.username, + password: auth.password, + logger, + }); - await remoteRepo.push(['refs/heads/master:refs/heads/master'], { - callbacks: { - credentials: credentialsProvider, - }, + await git.init({ + dir, + }); + + const paths = await globby(['./**', './**/.*'], { + cwd: dir, + gitignore: true, + dot: true, + }); + + for (const filepath of paths) { + await git.add({ dir, filepath }); + } + + await git.commit({ + dir, + message: 'Initial commit', + author: { name: 'Scaffolder', email: 'scaffolder@backstage.io' }, + committer: { name: 'Scaffolder', email: 'scaffolder@backstage.io' }, + }); + + await git.addRemote({ + dir, + url: remoteUrl, + remote: 'origin', + }); + + await git.push({ + dir, + remote: 'origin', }); } - -export async function pushToRemoteUserPass( - directory: string, - remote: string, - username: string, - password: string, -): Promise { - return pushToRemoteCred(directory, remote, () => - Cred.userpassPlaintextNew(username, password), - ); -} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index f3bc59e19d..bc6b63cc57 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -17,6 +17,7 @@ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { RequiredTemplateValues } from '../templater'; import { JsonValue } from '@backstage/config'; import { RemoteProtocol } from '../types'; +import { Logger } from 'winston'; /** * Publisher is in charge of taking a folder created by @@ -34,6 +35,7 @@ export type PublisherBase = { export type PublisherOptions = { values: RequiredTemplateValues & Record; + logger: Logger; directory: string; }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index ac3b1c80ec..ffd4d6487c 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -159,6 +159,7 @@ export async function createRouter( const result = await publisher.publish({ values: ctx.values, directory: ctx.resultDir, + logger: ctx.logger, }); return result; }, diff --git a/yarn.lock b/yarn.lock index b0a4720d11..957a7cebe4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4348,11 +4348,6 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== -"@sindresorhus/is@^2.0.0": - version "2.1.1" - resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz#ceff6a28a5b4867c2dd4a1ba513de278ccbe8bb1" - integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg== - "@sindresorhus/is@^3.1.1": version "3.1.2" resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz#548650de521b344e3781fbdb0ece4aa6f729afb8" @@ -5110,7 +5105,7 @@ dependencies: defer-to-connect "^1.0.1" -"@szmarczak/http-timer@^4.0.0", "@szmarczak/http-timer@^4.0.5": +"@szmarczak/http-timer@^4.0.5": version "4.0.5" resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== @@ -5822,7 +5817,7 @@ resolved "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== -"@types/keyv@*", "@types/keyv@^3.1.1": +"@types/keyv@*": version "3.1.1" resolved "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== @@ -5977,20 +5972,6 @@ resolved "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766" integrity sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw== -"@types/nodegit@0.26.11": - version "0.26.11" - resolved "https://registry.npmjs.org/@types/nodegit/-/nodegit-0.26.11.tgz#0cbc5e929f23e5ffc536920e3a887e0b3f46d1a4" - integrity sha512-BGrY9F8lBtfU+Ne1Pjb9k/PUsfSCUAqPXgKkTkM6mc5213H5VAoM12zG/sz/cr3jI3AXcngNbAYWlqSrXsWJug== - dependencies: - "@types/node" "*" - -"@types/nodegit@^0.26.12": - version "0.26.12" - resolved "https://registry.npmjs.org/@types/nodegit/-/nodegit-0.26.12.tgz#93afb4cb85d3a48d392c3232699c9c07d8251a36" - integrity sha512-4YpeTImFZNJ1cve4lEueHFVS8rAs8XpZqlmx+Bm9bMc+XMiCrcwaUf6peN7pod7Rl3esVlGP1zdBB7Z12eMVAA== - dependencies: - "@types/node" "*" - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -7643,6 +7624,11 @@ async-limiter@~1.0.0: resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-lock@^1.1.0: + version "1.2.4" + resolved "https://registry.npmjs.org/async-lock/-/async-lock-1.2.4.tgz#80d0d612383045dd0c30eb5aad08510c1397cb91" + integrity sha512-UBQJC2pbeyGutIfYmErGc9RaJYnpZ1FHaxuKwb0ahvGiiCkPUf3p67Io+YLPmmv3RHY+mF6JEtNW8FlHsraAaA== + async-retry@^1.2.1: version "1.3.1" resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" @@ -8280,14 +8266,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^1.0.0: - version "1.2.2" - resolved "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" - integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - bl@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" @@ -8546,19 +8524,6 @@ btoa@^1.2.1: resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -8569,11 +8534,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -8742,14 +8702,6 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -cacheable-lookup@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz#87be64a18b925234875e10a9bb1ebca4adce6b38" - integrity sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg== - dependencies: - "@types/keyv" "^3.1.1" - keyv "^4.0.0" - cacheable-lookup@^5.0.3: version "5.0.3" resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" @@ -9057,7 +9009,7 @@ chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3. optionalDependencies: fsevents "~2.1.2" -chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2: +chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -9121,6 +9073,11 @@ clean-css@^4.2.3: dependencies: source-map "~0.6.0" +clean-git-ref@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz#dcc0ca093b90e527e67adb5a5e55b1af6816dcd9" + integrity sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -10716,13 +10673,6 @@ decompress-response@^4.2.0: dependencies: mimic-response "^2.0.0" -decompress-response@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" - integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== - dependencies: - mimic-response "^2.0.0" - decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -10981,6 +10931,11 @@ diff-sequences@^26.6.2: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff3@0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz#d4e5c3a4cdf4e5fe1211ab42e693fcb4321580fc" + integrity sha1-1OXDpM305f4SEatC5pP8tDIVgPw= + diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -12866,7 +12821,7 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^7.0.0, fs-extra@^7.0.1: +fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== @@ -13438,27 +13393,6 @@ google-p12-pem@^3.0.3: dependencies: node-forge "^0.10.0" -got@^10.7.0: - version "10.7.0" - resolved "https://registry.npmjs.org/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f" - integrity sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg== - dependencies: - "@sindresorhus/is" "^2.0.0" - "@szmarczak/http-timer" "^4.0.0" - "@types/cacheable-request" "^6.0.1" - cacheable-lookup "^2.0.0" - cacheable-request "^7.0.1" - decompress-response "^5.0.0" - duplexer3 "^0.1.4" - get-stream "^5.0.0" - lowercase-keys "^2.0.0" - mimic-response "^2.1.0" - p-cancelable "^2.0.0" - p-event "^4.0.0" - responselike "^2.0.0" - to-readable-stream "^2.0.0" - type-fest "^0.10.0" - got@^11.5.2: version "11.6.0" resolved "https://registry.npmjs.org/got/-/got-11.6.0.tgz#4978c78f3cbc3a45ee95381f8bb6efd1db1f4638" @@ -15201,6 +15135,23 @@ isomorphic-form-data@~2.0.0: dependencies: form-data "^2.3.2" +isomorphic-git@^1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.8.0.tgz#50440650a64706a321cbea1af955c1cf1110b238" + integrity sha512-TWJvQh+++eFrEG0IFS/jLhMwsBoCOX1/Dsw9q8no59Mp1K0jEjSHXFWv2P04PwkxcIpePkXVBI5YFcFT2nkuQg== + dependencies: + async-lock "^1.1.0" + clean-git-ref "^2.0.1" + crc-32 "^1.2.0" + diff3 "0.0.3" + ignore "^5.1.4" + minimisted "^2.0.0" + pako "^1.0.10" + pify "^4.0.1" + readable-stream "^3.4.0" + sha.js "^2.4.9" + simple-get "^3.0.2" + isomorphic-ws@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" @@ -15944,7 +15895,7 @@ json3@^3.3.2: resolved "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== -json5@2.x, json5@^2.1.0, json5@^2.1.1, json5@^2.1.2: +json5@2.x, json5@^2.1.1, json5@^2.1.2: version "2.1.3" resolved "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== @@ -17456,7 +17407,7 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -mimic-response@^2.0.0, mimic-response@^2.1.0: +mimic-response@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== @@ -17526,6 +17477,13 @@ minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1. resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimisted@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz#d059fb905beecf0774bc3b308468699709805cb1" + integrity sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA== + dependencies: + minimist "^1.2.5" + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -17969,23 +17927,6 @@ node-gyp@3.x: tar "^2.0.0" which "1" -node-gyp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-4.0.0.tgz#972654af4e5dd0cd2a19081b4b46fe0442ba6f45" - integrity sha512-2XiryJ8sICNo6ej8d0idXDEMKfVfFK7kekGCtJAuelGsYHQxhj13KTf95swTCN2dZ/4lTfZ84Fu31jqJEEgjWA== - dependencies: - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^4.4.8" - which "1" - node-gyp@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332" @@ -18075,22 +18016,6 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-pre-gyp@^0.13.0: - version "0.13.0" - resolved "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz#df9ab7b68dd6498137717838e4f92a33fc9daa42" - integrity sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - node-releases@^1.1.52, node-releases@^1.1.58: version "1.1.60" resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" @@ -18119,21 +18044,6 @@ node-request-interceptor@^0.5.1: debug "^4.1.1" headers-utils "^1.2.0" -nodegit@0.27.0, nodegit@^0.27.0: - version "0.27.0" - resolved "https://registry.npmjs.org/nodegit/-/nodegit-0.27.0.tgz#4e8cc236f60e1c97324a5acff99056fe116a6ebe" - integrity sha512-E9K4gPjWiA0b3Tx5lfWCzG7Cvodi2idl3V5UD2fZrOrHikIfrN7Fc2kWLtMUqqomyoToYJLeIC8IV7xb1CYRLA== - dependencies: - fs-extra "^7.0.0" - got "^10.7.0" - json5 "^2.1.0" - lodash "^4.17.14" - nan "^2.14.0" - node-gyp "^4.0.0" - node-pre-gyp "^0.13.0" - ramda "^0.25.0" - tar-fs "^1.16.3" - nodemon@^2.0.2: version "2.0.6" resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz#1abe1937b463aaf62f0d52e2b7eaadf28cc2240d" @@ -18718,7 +18628,7 @@ p-each-series@^2.1.0: resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== -p-event@^4.0.0, p-event@^4.1.0: +p-event@^4.1.0: version "4.2.0" resolved "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ== @@ -18908,7 +18818,7 @@ packet-reader@1.0.0: resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== -pako@~1.0.5: +pako@^1.0.10, pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -20253,14 +20163,6 @@ public-encrypt@^4.0.0: randombytes "^2.0.1" safe-buffer "^5.1.2" -pump@^1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" - integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - pump@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -20406,11 +20308,6 @@ ramda@^0.21.0: resolved "https://registry.npmjs.org/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU= -ramda@^0.25.0: - version "0.25.0" - resolved "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" - integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ== - ramda@^0.26, ramda@~0.26.1: version "0.26.1" resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" @@ -21169,7 +21066,7 @@ read@1, read@~1.0.1: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -22243,7 +22140,7 @@ setprototypeof@1.2.0: resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: +sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8, sha.js@^2.4.9: version "2.4.11" resolved "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -22354,7 +22251,7 @@ simple-concat@^1.0.0: resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== -simple-get@^3.0.3: +simple-get@^3.0.2, simple-get@^3.0.3: version "3.1.0" resolved "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== @@ -23465,16 +23362,6 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tar-fs@^1.16.3: - version "1.16.3" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" - integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== - dependencies: - chownr "^1.0.1" - mkdirp "^0.5.1" - pump "^1.0.0" - tar-stream "^1.1.2" - tar-fs@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" @@ -23485,19 +23372,6 @@ tar-fs@~2.0.1: pump "^3.0.0" tar-stream "^2.0.0" -tar-stream@^1.1.2: - version "1.6.2" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" - integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== - dependencies: - bl "^1.0.0" - buffer-alloc "^1.2.0" - end-of-stream "^1.0.0" - fs-constants "^1.0.0" - readable-stream "^2.3.0" - to-buffer "^1.1.1" - xtend "^4.0.0" - tar-stream@^2.0.0, tar-stream@^2.1.4: version "2.1.4" resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" @@ -23837,11 +23711,6 @@ to-arraybuffer@^1.0.0: resolved "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= -to-buffer@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" - integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -23859,11 +23728,6 @@ to-readable-stream@^1.0.0: resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== -to-readable-stream@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" - integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== - to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -24164,11 +24028,6 @@ type-detect@4.0.8: resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.10.0: - version "0.10.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" - integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== - type-fest@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"