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 = {}));
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';

View File

@ -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')

View File

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

View File

@ -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<string> {
const args = ['show', `${ref}:${path}`]
const output = await this.exec(args, {encoding: 'base64'})
return output.stdout.trim()
}
async stashPush(options?: string[]): Promise<boolean> {
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<string> {
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<GitOutput> {
async exec(
args: string[],
{encoding = 'utf8', allowAllExitCodes = false}: ExecOpts = {}
): Promise<GitOutput> {
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
}
}

View File

@ -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'
})
)

View File

@ -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'