From 3cd5238e168880812d87110aea3fd6ad4b4a380f Mon Sep 17 00:00:00 2001 From: Cory Miller <13227161+cory-miller@users.noreply.github.com> Date: Tue, 13 Dec 2022 22:15:46 +0000 Subject: [PATCH] Implement branch list using callbacks from exec function --- __test__/git-command-manager.test.ts | 80 ++++++++++++++++++++++++++++ dist/index.js | 68 ++++++++++++++++------- src/git-command-manager.ts | 79 ++++++++++++++++++++------- 3 files changed, 188 insertions(+), 39 deletions(-) create mode 100644 __test__/git-command-manager.test.ts diff --git a/__test__/git-command-manager.test.ts b/__test__/git-command-manager.test.ts new file mode 100644 index 0000000..6944ff7 --- /dev/null +++ b/__test__/git-command-manager.test.ts @@ -0,0 +1,80 @@ +import * as exec from '@actions/exec' +import * as fshelper from '../lib/fs-helper' +import * as commandManager from '../lib/git-command-manager' + +let git: commandManager.IGitCommandManager +let mockExec = jest.fn() + +describe('git-auth-helper tests', () => { + beforeAll(async () => {}) + + beforeEach(async () => { + jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn()) + jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn()) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + afterAll(() => {}) + + it('branch list matches', async () => { + mockExec.mockImplementation((path, args, options) => { + console.log(args, options.listeners.stdout) + + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('2.18')) + return 0 + } + + if (args.includes('rev-parse')) { + options.listeners.stdline(Buffer.from('refs/heads/foo')) + options.listeners.stdline(Buffer.from('refs/heads/bar')) + return 0 + } + + return 1 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + const workingDirectory = 'test' + const lfs = false + git = await commandManager.createCommandManager(workingDirectory, lfs) + + let branches = await git.branchList(false) + + expect(branches).toHaveLength(2) + expect(branches.sort()).toEqual(['foo', 'bar'].sort()) + }) + + it('ambiguous ref name output is captured', async () => { + mockExec.mockImplementation((path, args, options) => { + console.log(args, options.listeners.stdout) + + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('2.18')) + return 0 + } + + if (args.includes('rev-parse')) { + options.listeners.stdline(Buffer.from('refs/heads/foo')) + // If refs/tags/v1 and refs/heads/tags/v1 existed on this repository + options.listeners.errline( + Buffer.from("error: refname 'tags/v1' is ambiguous") + ) + return 0 + } + + return 1 + }) + jest.spyOn(exec, 'exec').mockImplementation(mockExec) + const workingDirectory = 'test' + const lfs = false + git = await commandManager.createCommandManager(workingDirectory, lfs) + + let branches = await git.branchList(false) + + expect(branches).toHaveLength(1) + expect(branches.sort()).toEqual(['foo'].sort()) + }) +}) diff --git a/dist/index.js b/dist/index.js index 96be2f8..eb5ce60 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7441,8 +7441,10 @@ class GitCommandManager { const result = []; // Note, this implementation uses "rev-parse --symbolic-full-name" because the output from // "branch --list" is more difficult when in a detached HEAD state. - // Note, this implementation uses "rev-parse --symbolic-full-name" because there is a bug - // in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. + // TODO(https://github.com/actions/checkout/issues/786): this implementation uses + // "rev-parse --symbolic-full-name" because there is a bug + // in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When + // 2.18 is no longer supported, we can switch back to --symbolic. const args = ['rev-parse', '--symbolic-full-name']; if (remote) { args.push('--remotes=origin'); @@ -7450,18 +7452,42 @@ class GitCommandManager { else { args.push('--branches'); } - const output = yield this.execGit(args); - for (let branch of output.stdout.trim().split('\n')) { - branch = branch.trim(); - if (branch) { - if (branch.startsWith('refs/heads/')) { - branch = branch.substr('refs/heads/'.length); - } - else if (branch.startsWith('refs/remotes/')) { - branch = branch.substr('refs/remotes/'.length); - } - result.push(branch); + const stderr = []; + const errline = []; + const stdout = []; + const stdline = []; + const listeners = { + stderr: (data) => { + stderr.push(data.toString()); + }, + errline: (data) => { + errline.push(data.toString()); + }, + stdout: (data) => { + stdout.push(data.toString()); + }, + stdline: (data) => { + stdline.push(data.toString()); } + }; + // Suppress the output in order to avoid flooding annotations with innocuous errors. + yield this.execGit(args, false, true, listeners); + core.debug(`stderr callback is: ${stderr}`); + core.debug(`errline callback is: ${errline}`); + core.debug(`stdout callback is: ${stdout}`); + core.debug(`stdline callback is: ${stdline}`); + for (let branch of stdline) { + branch = branch.trim(); + if (!branch) { + continue; + } + if (branch.startsWith('refs/heads/')) { + branch = branch.substring('refs/heads/'.length); + } + else if (branch.startsWith('refs/remotes/')) { + branch = branch.substring('refs/remotes/'.length); + } + result.push(branch); } return result; }); @@ -7712,7 +7738,7 @@ class GitCommandManager { return result; }); } - execGit(args, allowAllExitCodes = false, silent = false) { + execGit(args, allowAllExitCodes = false, silent = false, customListeners = {}) { return __awaiter(this, void 0, void 0, function* () { fshelper.directoryExistsSync(this.workingDirectory, true); const result = new GitOutput(); @@ -7723,20 +7749,24 @@ class GitCommandManager { for (const key of Object.keys(this.gitEnv)) { env[key] = this.gitEnv[key]; } + const defaultListener = { + stdout: (data) => { + stdout.push(data.toString()); + } + }; + const mergedListeners = Object.assign(Object.assign({}, defaultListener), customListeners); const stdout = []; const options = { cwd: this.workingDirectory, env, silent, ignoreReturnCode: allowAllExitCodes, - listeners: { - stdout: (data) => { - stdout.push(data.toString()); - } - } + listeners: mergedListeners }; result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options); result.stdout = stdout.join(''); + core.debug(result.exitCode.toString()); + core.debug(result.stdout); return result; }); } diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 699a963..01aedfe 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -94,8 +94,11 @@ class GitCommandManager { // Note, this implementation uses "rev-parse --symbolic-full-name" because the output from // "branch --list" is more difficult when in a detached HEAD state. - // Note, this implementation uses "rev-parse --symbolic-full-name" because there is a bug - // in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. + + // TODO(https://github.com/actions/checkout/issues/786): this implementation uses + // "rev-parse --symbolic-full-name" because there is a bug + // in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When + // 2.18 is no longer supported, we can switch back to --symbolic. const args = ['rev-parse', '--symbolic-full-name'] if (remote) { @@ -104,21 +107,49 @@ class GitCommandManager { args.push('--branches') } - const output = await this.execGit(args) + const stderr: string[] = [] + const errline: string[] = [] + const stdout: string[] = [] + const stdline: string[] = [] - for (let branch of output.stdout.trim().split('\n')) { - branch = branch.trim() - if (branch) { - if (branch.startsWith('refs/heads/')) { - branch = branch.substr('refs/heads/'.length) - } else if (branch.startsWith('refs/remotes/')) { - branch = branch.substr('refs/remotes/'.length) - } - - result.push(branch) + const listeners = { + stderr: (data: Buffer) => { + stderr.push(data.toString()) + }, + errline: (data: Buffer) => { + errline.push(data.toString()) + }, + stdout: (data: Buffer) => { + stdout.push(data.toString()) + }, + stdline: (data: Buffer) => { + stdline.push(data.toString()) } } + // Suppress the output in order to avoid flooding annotations with innocuous errors. + await this.execGit(args, false, true, listeners) + + core.debug(`stderr callback is: ${stderr}`) + core.debug(`errline callback is: ${errline}`) + core.debug(`stdout callback is: ${stdout}`) + core.debug(`stdline callback is: ${stdline}`) + + for (let branch of stdline) { + branch = branch.trim() + if (!branch) { + continue + } + + if (branch.startsWith('refs/heads/')) { + branch = branch.substring('refs/heads/'.length) + } else if (branch.startsWith('refs/remotes/')) { + branch = branch.substring('refs/remotes/'.length) + } + + result.push(branch) + } + return result } @@ -395,7 +426,8 @@ class GitCommandManager { private async execGit( args: string[], allowAllExitCodes = false, - silent = false + silent = false, + customListeners = {} ): Promise { fshelper.directoryExistsSync(this.workingDirectory, true) @@ -409,22 +441,29 @@ class GitCommandManager { env[key] = this.gitEnv[key] } - const stdout: string[] = [] + const defaultListener = { + stdout: (data: Buffer) => { + stdout.push(data.toString()) + } + } + const mergedListeners = {...defaultListener, ...customListeners} + + const stdout: string[] = [] const options = { cwd: this.workingDirectory, env, silent, ignoreReturnCode: allowAllExitCodes, - listeners: { - stdout: (data: Buffer) => { - stdout.push(data.toString()) - } - } + listeners: mergedListeners } result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options) result.stdout = stdout.join('') + + core.debug(result.exitCode.toString()) + core.debug(result.stdout) + return result }