made git commands more generic and resusable

Signed-off-by: Jason Froehlich <jfroehlich12@gmail.com>
This commit is contained in:
Jason Froehlich
2024-01-16 12:07:23 -05:00
committed by blam
parent 76fffb321d
commit b8b8b88413
6 changed files with 455 additions and 96 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/plugin-scaffolder-node': minor
---
Added function to commitAndPushBranch which allows for files to be added to the a PR for use in the bitbucket pull request action for issue #21762
Added functions to clone a repo, create a branch, add files and push and commit to the branch. This allows for files to be added to the a PR for use in the bitbucket pull request action for issue #21762
@@ -23,10 +23,14 @@ import {
createTemplateAction,
getRepoSourceDirectory,
commitAndPushBranch,
addFiles,
createBranch as createGitBranch,
cloneRepo,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import fetch, { RequestInit, Response } from 'node-fetch';
import { Config } from '@backstage/config';
import fs from 'fs-extra';
const createPullRequest = async (opts: {
project: string;
@@ -354,10 +358,40 @@ export function createPublishBitbucketServerPullRequestAction(options: {
email: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const tempDir = await ctx.createTemporaryDirectory();
const sourceDir = getRepoSourceDirectory(ctx.workspacePath, undefined);
await cloneRepo({
url: remoteUrl,
dir: tempDir,
auth,
logger: ctx.logger,
ref: sourceBranch,
});
await createGitBranch({
dir: tempDir,
auth,
logger: ctx.logger,
ref: sourceBranch,
});
// copy files
fs.cpSync(sourceDir, tempDir, {
recursive: true,
filter: path => {
return !(path.indexOf('.git') > -1);
},
});
await addFiles({
dir: tempDir,
auth,
logger: ctx.logger,
filepath: '.',
});
await commitAndPushBranch({
tempDir: await ctx.createTemporaryDirectory(),
dir: getRepoSourceDirectory(ctx.workspacePath, undefined),
remoteUrl,
dir: tempDir,
auth,
logger: ctx.logger,
commitMessage:
+74 -10
View File
@@ -46,20 +46,14 @@ export type ActionContext<
};
// @public (undocumented)
export function commitAndPushBranch({
tempDir,
export function addFiles({
dir,
remoteUrl,
filepath,
auth,
logger,
commitMessage,
gitAuthorInfo,
branch,
remoteRef,
}: {
tempDir: string;
dir: string;
remoteUrl: string;
filepath: string;
auth:
| {
username: string;
@@ -68,7 +62,56 @@ export function commitAndPushBranch({
| {
token: string;
};
logger: Logger;
logger?: Logger | undefined;
}): Promise<void>;
// @public (undocumented)
export function cloneRepo({
url,
dir,
auth,
logger,
ref,
depth,
noCheckout,
}: {
url: string;
dir: string;
auth:
| {
username: string;
password: string;
}
| {
token: string;
};
logger?: Logger | undefined;
ref?: string | undefined;
depth?: number | undefined;
noCheckout?: boolean | undefined;
}): Promise<void>;
// @public (undocumented)
export function commitAndPushBranch({
dir,
auth,
logger,
commitMessage,
gitAuthorInfo,
branch,
remoteRef,
remote,
}: {
dir: string;
auth:
| {
username: string;
password: string;
}
| {
token: string;
};
logger?: Logger | undefined;
commitMessage: string;
gitAuthorInfo?: {
name?: string;
@@ -76,6 +119,7 @@ export function commitAndPushBranch({
};
branch?: string;
remoteRef?: string;
remote?: string;
}): Promise<{
commitHash: string;
}>;
@@ -103,6 +147,26 @@ export function commitAndPushRepo(input: {
commitHash: string;
}>;
// @public (undocumented)
export function createBranch({
dir,
auth,
logger,
ref,
}: {
dir: string;
ref: string;
auth:
| {
username: string;
password: string;
}
| {
token: string;
};
logger?: Logger | undefined;
}): Promise<void>;
// @public
export const createTemplateAction: <
TInputParams extends JsonObject = JsonObject,
@@ -19,6 +19,9 @@ import {
commitAndPushRepo,
initRepoAndPush,
commitAndPushBranch,
addFiles,
createBranch,
cloneRepo,
} from './gitHelpers';
jest.mock('@backstage/backend-common', () => ({
@@ -312,6 +315,198 @@ describe('commitAndPushRepo', () => {
});
});
describe('cloneRepo', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('with minimal parameters', () => {
beforeEach(async () => {
await cloneRepo({
url: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
});
});
it('clone the repo', () => {
expect(mockedGit.clone).toHaveBeenCalledWith({
url: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
ref: undefined,
depth: undefined,
noCheckout: undefined,
});
});
});
it('with token', async () => {
await cloneRepo({
url: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
auth: {
token: 'test-token',
},
});
expect(mockedGit.clone).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
url: 'git@github.com:test/repo.git',
ref: undefined,
depth: undefined,
noCheckout: undefined,
});
});
it('allows overriding the default branch', async () => {
await cloneRepo({
url: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
ref: 'trunk',
auth: {
username: 'test-user',
password: 'test-password',
},
});
expect(mockedGit.clone).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
url: 'git@github.com:test/repo.git',
ref: 'trunk',
depth: undefined,
noCheckout: undefined,
});
});
it('allows overriding the depth', async () => {
await cloneRepo({
url: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
ref: 'trunk',
depth: 2,
auth: {
username: 'test-user',
password: 'test-password',
},
});
expect(mockedGit.clone).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
url: 'git@github.com:test/repo.git',
ref: 'trunk',
depth: 2,
noCheckout: undefined,
});
});
it('allows overriding the noCheckout', async () => {
await cloneRepo({
url: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
ref: 'trunk',
depth: 2,
noCheckout: true,
auth: {
username: 'test-user',
password: 'test-password',
},
});
expect(mockedGit.clone).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
url: 'git@github.com:test/repo.git',
ref: 'trunk',
depth: 2,
noCheckout: true,
});
});
});
describe('createBranch', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('with minimal parameters', () => {
beforeEach(async () => {
await createBranch({
dir: '/tmp/repo/dir/',
ref: 'trunk',
auth: {
username: 'test-user',
password: 'test-password',
},
});
});
it('create the branch', () => {
expect(mockedGit.checkout).toHaveBeenCalledWith({
ref: 'trunk',
dir: '/tmp/repo/dir/',
});
});
});
it('with token', async () => {
await createBranch({
dir: '/tmp/repo/dir/',
ref: 'trunk',
auth: {
token: 'test-token',
},
});
expect(mockedGit.checkout).toHaveBeenCalledWith({
ref: 'trunk',
dir: '/tmp/repo/dir/',
});
});
});
describe('addFiles', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('with minimal parameters', () => {
beforeEach(async () => {
await addFiles({
dir: '/tmp/repo/dir/',
filepath: '.',
auth: {
username: 'test-user',
password: 'test-password',
},
});
});
it('add files', () => {
expect(mockedGit.add).toHaveBeenCalledWith({
filepath: '.',
dir: '/tmp/repo/dir/',
});
});
});
it('with token', async () => {
await addFiles({
dir: '/tmp/repo/dir/',
filepath: '.',
auth: {
token: 'test-token',
},
});
expect(mockedGit.add).toHaveBeenCalledWith({
filepath: '.',
dir: '/tmp/repo/dir/',
});
});
});
describe('commitAndPushBranch', () => {
afterEach(() => {
jest.clearAllMocks();
@@ -320,53 +515,19 @@ describe('commitAndPushBranch', () => {
describe('with minimal parameters', () => {
beforeEach(async () => {
await commitAndPushBranch({
tempDir: '/tmp/repo/dir/',
dir: '/test/repo/dir/',
remoteUrl: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
commitMessage: 'commit message',
gitAuthorInfo: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
logger: getVoidLogger(),
});
});
it('clone the repo', () => {
expect(mockedGit.clone).toHaveBeenCalledWith({
url: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
});
});
it('fetch the branches', () => {
expect(mockedGit.fetch).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
});
});
it('checkout the branch', () => {
expect(mockedGit.checkout).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
ref: 'master',
});
});
it('stages all files in the repo', () => {
expect(mockedGit.add).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
filepath: '.',
});
});
it('creates an commit', () => {
it('create commit', () => {
expect(mockedGit.commit).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
message: 'commit message',
dir: '/tmp/repo/dir/',
author: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
@@ -389,58 +550,91 @@ describe('commitAndPushBranch', () => {
it('with token', async () => {
await commitAndPushBranch({
tempDir: '/tmp/repo/dir/',
dir: '/test/repo/dir/',
remoteUrl: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
auth: {
token: 'test-token',
},
commitMessage: 'commit message',
gitAuthorInfo: {
name: 'gitCommitter',
email: 'gitCommitter@backstage.io',
},
logger: getVoidLogger(),
});
expect(mockedGit.clone).toHaveBeenCalledWith({
expect(mockedGit.commit).toHaveBeenCalledWith({
message: 'commit message',
dir: '/tmp/repo/dir/',
url: 'git@github.com:test/repo.git',
author: {
name: 'gitCommitter',
email: 'gitCommitter@backstage.io',
},
committer: {
name: 'gitCommitter',
email: 'gitCommitter@backstage.io',
},
});
expect(mockedGit.push).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
remote: 'origin',
remoteRef: 'refs/heads/master',
});
});
it('allows overriding the default branch', async () => {
await commitAndPushBranch({
tempDir: '/tmp/repo/dir/',
dir: '/test/repo/dir/',
branch: 'trunk',
remoteUrl: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
commitMessage: 'commit message',
logger: getVoidLogger(),
branch: 'trunk',
});
expect(mockedGit.checkout).toHaveBeenCalledWith({
expect(mockedGit.commit).toHaveBeenCalledWith({
message: 'commit message',
dir: '/tmp/repo/dir/',
ref: 'trunk',
author: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
committer: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
});
expect(mockedGit.push).toHaveBeenCalledWith({
dir: '/tmp/repo/dir/',
remote: 'origin',
remoteRef: 'refs/heads/trunk',
});
});
it('allows overriding the remoteRef', async () => {
await commitAndPushBranch({
tempDir: '/tmp/repo/dir/',
dir: '/test/repo/dir/',
gitAuthorInfo: {
name: 'Custom Scaffolder Author',
email: 'scaffolder@example.org',
},
remoteUrl: 'git@github.com:test/repo.git',
dir: '/tmp/repo/dir/',
auth: {
username: 'test-user',
password: 'test-password',
},
remoteRef: 'ABC123',
commitMessage: 'commit message',
logger: getVoidLogger(),
remoteRef: 'ABC123',
});
expect(mockedGit.commit).toHaveBeenCalledWith({
message: 'commit message',
dir: '/tmp/repo/dir/',
author: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
committer: {
name: 'Scaffolder',
email: 'scaffolder@backstage.io',
},
});
expect(mockedGit.push).toHaveBeenCalledWith({
@@ -16,7 +16,6 @@
import { Git } from '@backstage/backend-common';
import { Logger } from 'winston';
import fs from 'fs-extra';
/**
* @public
@@ -139,49 +138,114 @@ export async function commitAndPushRepo(input: {
/**
* @public
*/
export async function commitAndPushBranch({
tempDir,
export async function cloneRepo({
url,
dir,
auth,
logger,
ref,
depth,
noCheckout,
}: {
url: string;
dir: string;
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger?: Logger | undefined;
ref?: string | undefined;
depth?: number | undefined;
noCheckout?: boolean | undefined;
}): Promise<void> {
const git = Git.fromAuth({
...auth,
logger,
});
await git.clone({ url, dir, ref, depth, noCheckout });
}
/**
* @public
*/
export async function createBranch({
dir,
auth,
logger,
ref,
}: {
dir: string;
ref: string;
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger?: Logger | undefined;
}): Promise<void> {
const git = Git.fromAuth({
...auth,
logger,
});
await git.checkout({ dir, ref });
}
/**
* @public
*/
export async function addFiles({
dir,
filepath,
auth,
logger,
}: {
dir: string;
filepath: string;
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger?: Logger | undefined;
}): Promise<void> {
const git = Git.fromAuth({
...auth,
logger,
});
await git.add({ dir, filepath });
}
/**
* @public
*/
export async function commitAndPushBranch({
dir,
remoteUrl,
auth,
logger,
commitMessage,
gitAuthorInfo,
branch = 'master',
remoteRef,
remote = 'origin',
}: {
tempDir: string;
dir: string;
remoteUrl: string;
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger: Logger;
logger?: Logger | undefined;
commitMessage: string;
gitAuthorInfo?: { name?: string; email?: string };
branch?: string;
remoteRef?: string;
remote?: string;
}): Promise<{ commitHash: string }> {
const git = Git.fromAuth({
...auth,
logger,
});
await git.clone({ url: remoteUrl, dir: tempDir });
await git.fetch({ dir: tempDir });
await git.checkout({ dir: tempDir, ref: branch });
// copy files
fs.cpSync(dir, tempDir, {
recursive: true,
filter: path => {
return !(path.indexOf('.git') > -1);
},
});
await git.add({ dir: tempDir, filepath: '.' });
// use provided info if possible, otherwise use fallbacks
const authorInfo = {
name: gitAuthorInfo?.name ?? 'Scaffolder',
@@ -189,15 +253,15 @@ export async function commitAndPushBranch({
};
const commitHash = await git.commit({
dir: tempDir,
dir,
message: commitMessage,
author: authorInfo,
committer: authorInfo,
});
await git.push({
dir: tempDir,
remote: 'origin',
dir,
remote,
remoteRef: remoteRef ?? `refs/heads/${branch}`,
});
@@ -29,5 +29,8 @@ export {
initRepoAndPush,
commitAndPushRepo,
commitAndPushBranch,
addFiles,
createBranch,
cloneRepo,
} from './gitHelpers';
export { parseRepoUrl, getRepoSourceDirectory } from './util';