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 <alex.ford@determinate.systems>
This commit is contained in:
Graham Christensen 2025-02-24 06:36:54 -05:00 committed by GitHub
parent 367180cbdf
commit dd2324fc52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 71 additions and 56 deletions

59
dist/index.js vendored
View File

@ -67,7 +67,7 @@ var WorkingBaseType;
})(WorkingBaseType || (exports.WorkingBaseType = WorkingBaseType = {})); })(WorkingBaseType || (exports.WorkingBaseType = WorkingBaseType = {}));
function getWorkingBaseAndType(git) { function getWorkingBaseAndType(git) {
return __awaiter(this, void 0, void 0, function* () { 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) { if (symbolicRefResult.exitCode == 0) {
// A ref is checked out // A ref is checked out
return [symbolicRefResult.stdout.trim(), WorkingBaseType.Branch]; return [symbolicRefResult.stdout.trim(), WorkingBaseType.Branch];
@ -194,7 +194,7 @@ function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName
else { else {
aopts.push('-A'); aopts.push('-A');
} }
yield git.exec(aopts, true); yield git.exec(aopts, { allowAllExitCodes: true });
const popts = ['-m', commitMessage]; const popts = ['-m', commitMessage];
if (signoff) { if (signoff) {
popts.push('--signoff'); popts.push('--signoff');
@ -517,7 +517,7 @@ function createPullRequest(inputs) {
// Create signed commits via the GitHub API // Create signed commits via the GitHub API
const stashed = yield git.stashPush(['--include-untracked']); const stashed = yield git.stashPush(['--include-untracked']);
yield git.checkout(inputs.branch); 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-head-sha', pushSignedCommitsResult.sha);
outputs.set('pull-request-commits-verified', pushSignedCommitsResult.verified.toString()); outputs.set('pull-request-commits-verified', pushSignedCommitsResult.verified.toString());
yield git.checkout('-'); yield git.checkout('-');
@ -704,7 +704,7 @@ class GitCommandManager {
if (options) { if (options) {
args.push(...options); args.push(...options);
} }
return yield this.exec(args, allowAllExitCodes); return yield this.exec(args, { allowAllExitCodes: allowAllExitCodes });
}); });
} }
commit(options_1) { commit(options_1) {
@ -716,7 +716,7 @@ class GitCommandManager {
if (options) { if (options) {
args.push(...options); args.push(...options);
} }
return yield this.exec(args, allowAllExitCodes); return yield this.exec(args, { allowAllExitCodes: allowAllExitCodes });
}); });
} }
config(configKey, configValue, globalConfig, add) { config(configKey, configValue, globalConfig, add) {
@ -738,7 +738,7 @@ class GitCommandManager {
'--get-regexp', '--get-regexp',
configKey, configKey,
configValue configValue
], true); ], { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
@ -835,7 +835,7 @@ class GitCommandManager {
if (options) { if (options) {
args.push(...options); args.push(...options);
} }
const output = yield this.exec(args, true); const output = yield this.exec(args, { allowAllExitCodes: true });
return output.exitCode === 1; return output.exitCode === 1;
}); });
} }
@ -892,6 +892,13 @@ class GitCommandManager {
return output.stdout.trim(); 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) { stashPush(options) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const args = ['stash', 'push']; const args = ['stash', 'push'];
@ -939,13 +946,13 @@ class GitCommandManager {
'--unset', '--unset',
configKey, configKey,
configValue configValue
], true); ], { allowAllExitCodes: true });
return output.exitCode === 0; return output.exitCode === 0;
}); });
} }
tryGetRemoteUrl() { tryGetRemoteUrl() {
return __awaiter(this, void 0, void 0, function* () { 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) { if (output.exitCode !== 0) {
return ''; return '';
} }
@ -957,30 +964,34 @@ class GitCommandManager {
}); });
} }
exec(args_1) { 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 result = new GitOutput();
const env = {}; const env = {};
for (const key of Object.keys(process.env)) { for (const key of Object.keys(process.env)) {
env[key] = process.env[key]; env[key] = process.env[key];
} }
const stdout = []; const stdout = [];
let stdoutLength = 0;
const stderr = []; const stderr = [];
let stderrLength = 0;
const options = { const options = {
cwd: this.workingDirectory, cwd: this.workingDirectory,
env, env,
ignoreReturnCode: allowAllExitCodes, ignoreReturnCode: allowAllExitCodes,
listeners: { listeners: {
stdout: (data) => { stdout: (data) => {
stdout.push(data.toString()); stdout.push(data);
stdoutLength += data.length;
}, },
stderr: (data) => { stderr: (data) => {
stderr.push(data.toString()); stderr.push(data);
stderrLength += data.length;
} }
} }
}; };
result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options); result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options);
result.stdout = stdout.join(''); result.stdout = Buffer.concat(stdout, stdoutLength).toString(encoding);
result.stderr = stderr.join(''); result.stderr = Buffer.concat(stderr, stderrLength).toString(encoding);
return result; return result;
}); });
} }
@ -1400,7 +1411,7 @@ class GitHubHelper {
return pull; return pull;
}); });
} }
pushSignedCommits(branchCommits, baseCommit, repoPath, branchRepository, branch) { pushSignedCommits(git, branchCommits, baseCommit, repoPath, branchRepository, branch) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let headCommit = { let headCommit = {
sha: baseCommit.sha, sha: baseCommit.sha,
@ -1408,13 +1419,13 @@ class GitHubHelper {
verified: false verified: false
}; };
for (const commit of branchCommits) { 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); yield this.createOrUpdateRef(branchRepository, branch, headCommit.sha);
return headCommit; return headCommit;
}); });
} }
createCommit(commit, parentCommit, repoPath, branchRepository) { createCommit(git, commit, parentCommit, repoPath, branchRepository) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const repository = this.parseRepository(branchRepository); const repository = this.parseRepository(branchRepository);
// In the case of an empty commit, the tree references the parent's tree // In the case of an empty commit, the tree references the parent's tree
@ -1436,7 +1447,9 @@ class GitHubHelper {
let sha = null; let sha = null;
if (status === 'A' || status === 'M') { if (status === 'A' || status === 'M') {
try { 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; sha = blob.sha;
} }
catch (error) { catch (error) {
@ -1763,7 +1776,6 @@ exports.randomString = randomString;
exports.parseDisplayNameEmail = parseDisplayNameEmail; exports.parseDisplayNameEmail = parseDisplayNameEmail;
exports.fileExistsSync = fileExistsSync; exports.fileExistsSync = fileExistsSync;
exports.readFile = readFile; exports.readFile = readFile;
exports.readFileBase64 = readFileBase64;
exports.getErrorMessage = getErrorMessage; exports.getErrorMessage = getErrorMessage;
const core = __importStar(__nccwpck_require__(7484)); const core = __importStar(__nccwpck_require__(7484));
const fs = __importStar(__nccwpck_require__(9896)); const fs = __importStar(__nccwpck_require__(9896));
@ -1853,15 +1865,6 @@ function fileExistsSync(path) {
function readFile(path) { function readFile(path) {
return fs.readFileSync(path, 'utf-8'); 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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
function hasErrorCode(error) { function hasErrorCode(error) {
return typeof (error && error.code) === 'string'; return typeof (error && error.code) === 'string';

View File

@ -19,7 +19,7 @@ export async function getWorkingBaseAndType(
): Promise<[string, WorkingBaseType]> { ): Promise<[string, WorkingBaseType]> {
const symbolicRefResult = await git.exec( const symbolicRefResult = await git.exec(
['symbolic-ref', 'HEAD', '--short'], ['symbolic-ref', 'HEAD', '--short'],
true {allowAllExitCodes: true}
) )
if (symbolicRefResult.exitCode == 0) { if (symbolicRefResult.exitCode == 0) {
// A ref is checked out // A ref is checked out
@ -200,7 +200,7 @@ export async function createOrUpdateBranch(
} else { } else {
aopts.push('-A') aopts.push('-A')
} }
await git.exec(aopts, true) await git.exec(aopts, {allowAllExitCodes: true})
const popts = ['-m', commitMessage] const popts = ['-m', commitMessage]
if (signoff) { if (signoff) {
popts.push('--signoff') popts.push('--signoff')

View File

@ -211,6 +211,7 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
const stashed = await git.stashPush(['--include-untracked']) const stashed = await git.stashPush(['--include-untracked'])
await git.checkout(inputs.branch) await git.checkout(inputs.branch)
const pushSignedCommitsResult = await ghBranch.pushSignedCommits( const pushSignedCommitsResult = await ghBranch.pushSignedCommits(
git,
result.branchCommits, result.branchCommits,
result.baseCommit, result.baseCommit,
repoPath, repoPath,

View File

@ -21,6 +21,11 @@ export type Commit = {
unparsedChanges: string[] unparsedChanges: string[]
} }
export type ExecOpts = {
allowAllExitCodes?: boolean
encoding?: 'utf8' | 'base64'
}
export class GitCommandManager { export class GitCommandManager {
private gitPath: string private gitPath: string
private workingDirectory: string private workingDirectory: string
@ -66,7 +71,7 @@ export class GitCommandManager {
args.push(...options) args.push(...options)
} }
return await this.exec(args, allowAllExitCodes) return await this.exec(args, {allowAllExitCodes: allowAllExitCodes})
} }
async commit( async commit(
@ -82,7 +87,7 @@ export class GitCommandManager {
args.push(...options) args.push(...options)
} }
return await this.exec(args, allowAllExitCodes) return await this.exec(args, {allowAllExitCodes: allowAllExitCodes})
} }
async config( async config(
@ -113,7 +118,7 @@ export class GitCommandManager {
configKey, configKey,
configValue configValue
], ],
true {allowAllExitCodes: true}
) )
return output.exitCode === 0 return output.exitCode === 0
} }
@ -222,7 +227,7 @@ export class GitCommandManager {
if (options) { if (options) {
args.push(...options) args.push(...options)
} }
const output = await this.exec(args, true) const output = await this.exec(args, {allowAllExitCodes: true})
return output.exitCode === 1 return output.exitCode === 1
} }
@ -278,6 +283,12 @@ export class GitCommandManager {
return output.stdout.trim() return output.stdout.trim()
} }
async showFileAtRefBase64(ref: string, path: string): Promise<string> {
const args = ['show', `${ref}:${path}`]
const output = await this.exec(args, {encoding: 'base64'})
return output.stdout.trim()
}
async stashPush(options?: string[]): Promise<boolean> { async stashPush(options?: string[]): Promise<boolean> {
const args = ['stash', 'push'] const args = ['stash', 'push']
if (options) { if (options) {
@ -326,7 +337,7 @@ export class GitCommandManager {
configKey, configKey,
configValue configValue
], ],
true {allowAllExitCodes: true}
) )
return output.exitCode === 0 return output.exitCode === 0
} }
@ -334,7 +345,7 @@ export class GitCommandManager {
async tryGetRemoteUrl(): Promise<string> { async tryGetRemoteUrl(): Promise<string> {
const output = await this.exec( const output = await this.exec(
['config', '--local', '--get', 'remote.origin.url'], ['config', '--local', '--get', 'remote.origin.url'],
true {allowAllExitCodes: true}
) )
if (output.exitCode !== 0) { if (output.exitCode !== 0) {
@ -349,7 +360,10 @@ export class GitCommandManager {
return stdout return stdout
} }
async exec(args: string[], allowAllExitCodes = false): Promise<GitOutput> { async exec(
args: string[],
{encoding = 'utf8', allowAllExitCodes = false}: ExecOpts = {}
): Promise<GitOutput> {
const result = new GitOutput() const result = new GitOutput()
const env = {} const env = {}
@ -357,8 +371,10 @@ export class GitCommandManager {
env[key] = process.env[key] env[key] = process.env[key]
} }
const stdout: string[] = [] const stdout: Buffer[] = []
const stderr: string[] = [] let stdoutLength = 0
const stderr: Buffer[] = []
let stderrLength = 0
const options = { const options = {
cwd: this.workingDirectory, cwd: this.workingDirectory,
@ -366,17 +382,19 @@ export class GitCommandManager {
ignoreReturnCode: allowAllExitCodes, ignoreReturnCode: allowAllExitCodes,
listeners: { listeners: {
stdout: (data: Buffer) => { stdout: (data: Buffer) => {
stdout.push(data.toString()) stdout.push(data)
stdoutLength += data.length
}, },
stderr: (data: Buffer) => { stderr: (data: Buffer) => {
stderr.push(data.toString()) stderr.push(data)
stderrLength += data.length
} }
} }
} }
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options) result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('') result.stdout = Buffer.concat(stdout, stdoutLength).toString(encoding)
result.stderr = stderr.join('') result.stderr = Buffer.concat(stderr, stderrLength).toString(encoding)
return result return result
} }
} }

View File

@ -1,6 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Inputs} from './create-pull-request' 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 {Octokit, OctokitOptions, throttleOptions} from './octokit-client'
import pLimit from 'p-limit' import pLimit from 'p-limit'
import * as utils from './utils' import * as utils from './utils'
@ -220,6 +220,7 @@ export class GitHubHelper {
} }
async pushSignedCommits( async pushSignedCommits(
git: GitCommandManager,
branchCommits: Commit[], branchCommits: Commit[],
baseCommit: Commit, baseCommit: Commit,
repoPath: string, repoPath: string,
@ -233,6 +234,7 @@ export class GitHubHelper {
} }
for (const commit of branchCommits) { for (const commit of branchCommits) {
headCommit = await this.createCommit( headCommit = await this.createCommit(
git,
commit, commit,
headCommit, headCommit,
repoPath, repoPath,
@ -244,6 +246,7 @@ export class GitHubHelper {
} }
private async createCommit( private async createCommit(
git: GitCommandManager,
commit: Commit, commit: Commit,
parentCommit: CommitResponse, parentCommit: CommitResponse,
repoPath: string, repoPath: string,
@ -269,10 +272,10 @@ export class GitHubHelper {
let sha: string | null = null let sha: string | null = null
if (status === 'A' || status === 'M') { if (status === 'A' || status === 'M') {
try { try {
const {data: blob} = await blobCreationLimit(() => const {data: blob} = await blobCreationLimit(async () =>
this.octokit.rest.git.createBlob({ this.octokit.rest.git.createBlob({
...repository, ...repository,
content: utils.readFileBase64([repoPath, path]), content: await git.showFileAtRefBase64(commit.sha, path),
encoding: 'base64' encoding: 'base64'
}) })
) )

View File

@ -126,16 +126,6 @@ export function readFile(path: string): string {
return fs.readFileSync(path, 'utf-8') 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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
function hasErrorCode(error: any): error is {code: string} { function hasErrorCode(error: any): error is {code: string} {
return typeof (error && error.code) === 'string' return typeof (error && error.code) === 'string'