From dd2324fc52d5d43c699a5636bcf19fceaa70c284 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 24 Feb 2025 06:36:54 -0500 Subject: [PATCH] fix: use showFileAtRefBase64 to read per-commit file contents (#3744) * GitCommandManager: add a function to get a file's contents at a specific revision * use showFileAtRef instead of readFileBase64 * Teach GitCommandManager.exec about an object of exec parameters so we can add more * Encode the showFiletRef output as base64 out of the gate * Fix missing async for function * Use Buffer.concat to avoid issues with partial data streams * formatting --------- Co-authored-by: gustavderdrache --- dist/index.js | 59 ++++++++++++++++++---------------- src/create-or-update-branch.ts | 4 +-- src/create-pull-request.ts | 1 + src/git-command-manager.ts | 44 +++++++++++++++++-------- src/github-helper.ts | 9 ++++-- src/utils.ts | 10 ------ 6 files changed, 71 insertions(+), 56 deletions(-) diff --git a/dist/index.js b/dist/index.js index 1f1f609..0ce0204 100644 --- a/dist/index.js +++ b/dist/index.js @@ -67,7 +67,7 @@ var WorkingBaseType; })(WorkingBaseType || (exports.WorkingBaseType = WorkingBaseType = {})); function getWorkingBaseAndType(git) { return __awaiter(this, void 0, void 0, function* () { - const symbolicRefResult = yield git.exec(['symbolic-ref', 'HEAD', '--short'], true); + const symbolicRefResult = yield git.exec(['symbolic-ref', 'HEAD', '--short'], { allowAllExitCodes: true }); if (symbolicRefResult.exitCode == 0) { // A ref is checked out return [symbolicRefResult.stdout.trim(), WorkingBaseType.Branch]; @@ -194,7 +194,7 @@ function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName else { aopts.push('-A'); } - yield git.exec(aopts, true); + yield git.exec(aopts, { allowAllExitCodes: true }); const popts = ['-m', commitMessage]; if (signoff) { popts.push('--signoff'); @@ -517,7 +517,7 @@ function createPullRequest(inputs) { // Create signed commits via the GitHub API const stashed = yield git.stashPush(['--include-untracked']); yield git.checkout(inputs.branch); - const pushSignedCommitsResult = yield ghBranch.pushSignedCommits(result.branchCommits, result.baseCommit, repoPath, branchRepository, inputs.branch); + const pushSignedCommitsResult = yield ghBranch.pushSignedCommits(git, result.branchCommits, result.baseCommit, repoPath, branchRepository, inputs.branch); outputs.set('pull-request-head-sha', pushSignedCommitsResult.sha); outputs.set('pull-request-commits-verified', pushSignedCommitsResult.verified.toString()); yield git.checkout('-'); @@ -704,7 +704,7 @@ class GitCommandManager { if (options) { args.push(...options); } - return yield this.exec(args, allowAllExitCodes); + return yield this.exec(args, { allowAllExitCodes: allowAllExitCodes }); }); } commit(options_1) { @@ -716,7 +716,7 @@ class GitCommandManager { if (options) { args.push(...options); } - return yield this.exec(args, allowAllExitCodes); + return yield this.exec(args, { allowAllExitCodes: allowAllExitCodes }); }); } config(configKey, configValue, globalConfig, add) { @@ -738,7 +738,7 @@ class GitCommandManager { '--get-regexp', configKey, configValue - ], true); + ], { allowAllExitCodes: true }); return output.exitCode === 0; }); } @@ -835,7 +835,7 @@ class GitCommandManager { if (options) { args.push(...options); } - const output = yield this.exec(args, true); + const output = yield this.exec(args, { allowAllExitCodes: true }); return output.exitCode === 1; }); } @@ -892,6 +892,13 @@ class GitCommandManager { return output.stdout.trim(); }); } + showFileAtRefBase64(ref, path) { + return __awaiter(this, void 0, void 0, function* () { + const args = ['show', `${ref}:${path}`]; + const output = yield this.exec(args, { encoding: 'base64' }); + return output.stdout.trim(); + }); + } stashPush(options) { return __awaiter(this, void 0, void 0, function* () { const args = ['stash', 'push']; @@ -939,13 +946,13 @@ class GitCommandManager { '--unset', configKey, configValue - ], true); + ], { allowAllExitCodes: true }); return output.exitCode === 0; }); } tryGetRemoteUrl() { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.exec(['config', '--local', '--get', 'remote.origin.url'], true); + const output = yield this.exec(['config', '--local', '--get', 'remote.origin.url'], { allowAllExitCodes: true }); if (output.exitCode !== 0) { return ''; } @@ -957,30 +964,34 @@ class GitCommandManager { }); } exec(args_1) { - return __awaiter(this, arguments, void 0, function* (args, allowAllExitCodes = false) { + return __awaiter(this, arguments, void 0, function* (args, { encoding = 'utf8', allowAllExitCodes = false } = {}) { const result = new GitOutput(); const env = {}; for (const key of Object.keys(process.env)) { env[key] = process.env[key]; } const stdout = []; + let stdoutLength = 0; const stderr = []; + let stderrLength = 0; const options = { cwd: this.workingDirectory, env, ignoreReturnCode: allowAllExitCodes, listeners: { stdout: (data) => { - stdout.push(data.toString()); + stdout.push(data); + stdoutLength += data.length; }, stderr: (data) => { - stderr.push(data.toString()); + stderr.push(data); + stderrLength += data.length; } } }; result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options); - result.stdout = stdout.join(''); - result.stderr = stderr.join(''); + result.stdout = Buffer.concat(stdout, stdoutLength).toString(encoding); + result.stderr = Buffer.concat(stderr, stderrLength).toString(encoding); return result; }); } @@ -1400,7 +1411,7 @@ class GitHubHelper { return pull; }); } - pushSignedCommits(branchCommits, baseCommit, repoPath, branchRepository, branch) { + pushSignedCommits(git, branchCommits, baseCommit, repoPath, branchRepository, branch) { return __awaiter(this, void 0, void 0, function* () { let headCommit = { sha: baseCommit.sha, @@ -1408,13 +1419,13 @@ class GitHubHelper { verified: false }; for (const commit of branchCommits) { - headCommit = yield this.createCommit(commit, headCommit, repoPath, branchRepository); + headCommit = yield this.createCommit(git, commit, headCommit, repoPath, branchRepository); } yield this.createOrUpdateRef(branchRepository, branch, headCommit.sha); return headCommit; }); } - createCommit(commit, parentCommit, repoPath, branchRepository) { + createCommit(git, commit, parentCommit, repoPath, branchRepository) { return __awaiter(this, void 0, void 0, function* () { const repository = this.parseRepository(branchRepository); // In the case of an empty commit, the tree references the parent's tree @@ -1436,7 +1447,9 @@ class GitHubHelper { let sha = null; if (status === 'A' || status === 'M') { try { - const { data: blob } = yield blobCreationLimit(() => this.octokit.rest.git.createBlob(Object.assign(Object.assign({}, repository), { content: utils.readFileBase64([repoPath, path]), encoding: 'base64' }))); + const { data: blob } = yield blobCreationLimit(() => __awaiter(this, void 0, void 0, function* () { + return this.octokit.rest.git.createBlob(Object.assign(Object.assign({}, repository), { content: yield git.showFileAtRefBase64(commit.sha, path), encoding: 'base64' })); + })); sha = blob.sha; } catch (error) { @@ -1763,7 +1776,6 @@ exports.randomString = randomString; exports.parseDisplayNameEmail = parseDisplayNameEmail; exports.fileExistsSync = fileExistsSync; exports.readFile = readFile; -exports.readFileBase64 = readFileBase64; exports.getErrorMessage = getErrorMessage; const core = __importStar(__nccwpck_require__(7484)); const fs = __importStar(__nccwpck_require__(9896)); @@ -1853,15 +1865,6 @@ function fileExistsSync(path) { function readFile(path) { return fs.readFileSync(path, 'utf-8'); } -function readFileBase64(pathParts) { - const resolvedPath = path.resolve(...pathParts); - if (fs.lstatSync(resolvedPath).isSymbolicLink()) { - return fs - .readlinkSync(resolvedPath, { encoding: 'buffer' }) - .toString('base64'); - } - return fs.readFileSync(resolvedPath).toString('base64'); -} /* eslint-disable @typescript-eslint/no-explicit-any */ function hasErrorCode(error) { return typeof (error && error.code) === 'string'; diff --git a/src/create-or-update-branch.ts b/src/create-or-update-branch.ts index 2eecd45..746a3a0 100644 --- a/src/create-or-update-branch.ts +++ b/src/create-or-update-branch.ts @@ -19,7 +19,7 @@ export async function getWorkingBaseAndType( ): Promise<[string, WorkingBaseType]> { const symbolicRefResult = await git.exec( ['symbolic-ref', 'HEAD', '--short'], - true + {allowAllExitCodes: true} ) if (symbolicRefResult.exitCode == 0) { // A ref is checked out @@ -200,7 +200,7 @@ export async function createOrUpdateBranch( } else { aopts.push('-A') } - await git.exec(aopts, true) + await git.exec(aopts, {allowAllExitCodes: true}) const popts = ['-m', commitMessage] if (signoff) { popts.push('--signoff') diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 9076b7d..0ab4f8c 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -211,6 +211,7 @@ export async function createPullRequest(inputs: Inputs): Promise { const stashed = await git.stashPush(['--include-untracked']) await git.checkout(inputs.branch) const pushSignedCommitsResult = await ghBranch.pushSignedCommits( + git, result.branchCommits, result.baseCommit, repoPath, diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 9be35e4..54cad08 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -21,6 +21,11 @@ export type Commit = { unparsedChanges: string[] } +export type ExecOpts = { + allowAllExitCodes?: boolean + encoding?: 'utf8' | 'base64' +} + export class GitCommandManager { private gitPath: string private workingDirectory: string @@ -66,7 +71,7 @@ export class GitCommandManager { args.push(...options) } - return await this.exec(args, allowAllExitCodes) + return await this.exec(args, {allowAllExitCodes: allowAllExitCodes}) } async commit( @@ -82,7 +87,7 @@ export class GitCommandManager { args.push(...options) } - return await this.exec(args, allowAllExitCodes) + return await this.exec(args, {allowAllExitCodes: allowAllExitCodes}) } async config( @@ -113,7 +118,7 @@ export class GitCommandManager { configKey, configValue ], - true + {allowAllExitCodes: true} ) return output.exitCode === 0 } @@ -222,7 +227,7 @@ export class GitCommandManager { if (options) { args.push(...options) } - const output = await this.exec(args, true) + const output = await this.exec(args, {allowAllExitCodes: true}) return output.exitCode === 1 } @@ -278,6 +283,12 @@ export class GitCommandManager { return output.stdout.trim() } + async showFileAtRefBase64(ref: string, path: string): Promise { + const args = ['show', `${ref}:${path}`] + const output = await this.exec(args, {encoding: 'base64'}) + return output.stdout.trim() + } + async stashPush(options?: string[]): Promise { const args = ['stash', 'push'] if (options) { @@ -326,7 +337,7 @@ export class GitCommandManager { configKey, configValue ], - true + {allowAllExitCodes: true} ) return output.exitCode === 0 } @@ -334,7 +345,7 @@ export class GitCommandManager { async tryGetRemoteUrl(): Promise { const output = await this.exec( ['config', '--local', '--get', 'remote.origin.url'], - true + {allowAllExitCodes: true} ) if (output.exitCode !== 0) { @@ -349,7 +360,10 @@ export class GitCommandManager { return stdout } - async exec(args: string[], allowAllExitCodes = false): Promise { + async exec( + args: string[], + {encoding = 'utf8', allowAllExitCodes = false}: ExecOpts = {} + ): Promise { const result = new GitOutput() const env = {} @@ -357,8 +371,10 @@ export class GitCommandManager { env[key] = process.env[key] } - const stdout: string[] = [] - const stderr: string[] = [] + const stdout: Buffer[] = [] + let stdoutLength = 0 + const stderr: Buffer[] = [] + let stderrLength = 0 const options = { cwd: this.workingDirectory, @@ -366,17 +382,19 @@ export class GitCommandManager { ignoreReturnCode: allowAllExitCodes, listeners: { stdout: (data: Buffer) => { - stdout.push(data.toString()) + stdout.push(data) + stdoutLength += data.length }, stderr: (data: Buffer) => { - stderr.push(data.toString()) + stderr.push(data) + stderrLength += data.length } } } result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options) - result.stdout = stdout.join('') - result.stderr = stderr.join('') + result.stdout = Buffer.concat(stdout, stdoutLength).toString(encoding) + result.stderr = Buffer.concat(stderr, stderrLength).toString(encoding) return result } } diff --git a/src/github-helper.ts b/src/github-helper.ts index 82a9296..6d41e06 100644 --- a/src/github-helper.ts +++ b/src/github-helper.ts @@ -1,6 +1,6 @@ import * as core from '@actions/core' import {Inputs} from './create-pull-request' -import {Commit} from './git-command-manager' +import {Commit, GitCommandManager} from './git-command-manager' import {Octokit, OctokitOptions, throttleOptions} from './octokit-client' import pLimit from 'p-limit' import * as utils from './utils' @@ -220,6 +220,7 @@ export class GitHubHelper { } async pushSignedCommits( + git: GitCommandManager, branchCommits: Commit[], baseCommit: Commit, repoPath: string, @@ -233,6 +234,7 @@ export class GitHubHelper { } for (const commit of branchCommits) { headCommit = await this.createCommit( + git, commit, headCommit, repoPath, @@ -244,6 +246,7 @@ export class GitHubHelper { } private async createCommit( + git: GitCommandManager, commit: Commit, parentCommit: CommitResponse, repoPath: string, @@ -269,10 +272,10 @@ export class GitHubHelper { let sha: string | null = null if (status === 'A' || status === 'M') { try { - const {data: blob} = await blobCreationLimit(() => + const {data: blob} = await blobCreationLimit(async () => this.octokit.rest.git.createBlob({ ...repository, - content: utils.readFileBase64([repoPath, path]), + content: await git.showFileAtRefBase64(commit.sha, path), encoding: 'base64' }) ) diff --git a/src/utils.ts b/src/utils.ts index ced3cbd..b501dd4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -126,16 +126,6 @@ export function readFile(path: string): string { return fs.readFileSync(path, 'utf-8') } -export function readFileBase64(pathParts: string[]): string { - const resolvedPath = path.resolve(...pathParts) - if (fs.lstatSync(resolvedPath).isSymbolicLink()) { - return fs - .readlinkSync(resolvedPath, {encoding: 'buffer'}) - .toString('base64') - } - return fs.readFileSync(resolvedPath).toString('base64') -} - /* eslint-disable @typescript-eslint/no-explicit-any */ function hasErrorCode(error: any): error is {code: string} { return typeof (error && error.code) === 'string'