Reworking.
This commit is contained in:
+166
-231
@@ -1,23 +1,68 @@
|
||||
import core from '@actions/core';
|
||||
import exec from '@actions/exec';
|
||||
import github from '@actions/github';
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
import { PushEvent } from '@octokit/webhooks-types';
|
||||
import { unlinkSync, writeFileSync } from 'fs';
|
||||
|
||||
import { Gitea } from '@go-gitea/sdk.js';
|
||||
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import { outro } from '@clack/prompts';
|
||||
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
||||
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
||||
import { sleep } from './utils/sleep';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Types */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
// This should be a token with access to your repository scoped in as a secret.
|
||||
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN');
|
||||
const PLATFORM = core.getInput('PLATFORM') || 'github';
|
||||
const DRY_RUN = core.getInput('DRY_RUN') === 'true';
|
||||
const GITEA_URL = core.getInput('GITEA_URL') || 'https://try.gitea.io';
|
||||
|
||||
// Initialize platform-specific clients
|
||||
let octokit: ReturnType<typeof github.getOctokit>;
|
||||
let giteaClient: any; // We'll initialize this properly below
|
||||
|
||||
if (PLATFORM === 'gitea' || PLATFORM === 'forgejo') {
|
||||
// Dynamically import the Gitea client
|
||||
try {
|
||||
const GiteaClient = require('@go-gitea/sdk.js').GiteaClient;
|
||||
giteaClient = new GiteaClient(GITEA_URL, {
|
||||
token: GITHUB_TOKEN
|
||||
});
|
||||
} catch (error) {
|
||||
core.error(`Failed to import Gitea client: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
octokit = github.getOctokit(GITHUB_TOKEN);
|
||||
}
|
||||
|
||||
const context = github.context;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
type SHA = string;
|
||||
type Diff = string;
|
||||
|
||||
async function getCommitDiff(commitSha: string): Promise<{ sha: SHA; diff: Diff }> {
|
||||
if (PLATFORM === 'gitea' || PLATFORM === 'forgejo') {
|
||||
try {
|
||||
const commit = await giteaClient.getCommit(owner, repo, commitSha);
|
||||
const diffResponse = await giteaClient.getRepoCommitDiff(owner, repo, commitSha);
|
||||
return { sha: commitSha, diff: diffResponse.data };
|
||||
} catch (error) {
|
||||
core.error(`Failed to fetch Gitea/Forgejo commit diff: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const diffResponse = await octokit.request<string>(
|
||||
'GET /repos/{owner}/{repo}/commits/{ref}',
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
ref: commitSha,
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3.diff'
|
||||
}
|
||||
}
|
||||
);
|
||||
return { sha: commitSha, diff: diffResponse.data };
|
||||
}
|
||||
}
|
||||
|
||||
interface DiffAndSHA {
|
||||
sha: SHA;
|
||||
diff: Diff;
|
||||
@@ -28,257 +73,147 @@ interface MsgAndSHA {
|
||||
msg: string;
|
||||
}
|
||||
|
||||
type Platform = 'github' | 'gitea';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Constants */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
const OPENCOMMIT_MARKER = /(opencommit|🤖)/i;
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Platform detection */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
function detectPlatform(): Platform {
|
||||
if (process.env.GITEA_ACTIONS === 'true') return 'gitea';
|
||||
if (process.env.FORGEJO_ACTIONS === 'true') return 'gitea';
|
||||
return 'github';
|
||||
}
|
||||
|
||||
const platform = detectPlatform();
|
||||
const dryRun = core.getBooleanInput('dry_run');
|
||||
|
||||
const context = github.context;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Clients */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN');
|
||||
|
||||
const octokit =
|
||||
platform === 'github' ? github.getOctokit(GITHUB_TOKEN) : null;
|
||||
|
||||
const gitea =
|
||||
platform === 'gitea'
|
||||
? new Gitea()
|
||||
: null;
|
||||
|
||||
const giteaBaseUrl =
|
||||
process.env.GITEA_SERVER_URL ??
|
||||
process.env.GITHUB_SERVER_URL;
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
function shouldSkipCommit(message: string): boolean {
|
||||
if (/^merge\b/i.test(message)) return true;
|
||||
if (OPENCOMMIT_MARKER.test(message)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getCommitDiff(commitSha: string): Promise<{ sha: SHA; diff: Diff }> {
|
||||
if (platform === 'github') {
|
||||
const res = await octokit!.request<string>(
|
||||
'GET /repos/{owner}/{repo}/commits/{ref}',
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
ref: commitSha,
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3.diff',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { sha: commitSha, diff: res.data };
|
||||
}
|
||||
|
||||
const { data } = await gitea!.request({
|
||||
method: 'GET',
|
||||
url: `${giteaBaseUrl}/repos/${owner}/${repo}/git/commits/${commitSha}`,
|
||||
headers: {
|
||||
Accept: 'application/vnd.gitea.v1.diff',
|
||||
Authorization: `token ${GITHUB_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
return { sha: commitSha, diff: data as string };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Diff fetching */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
const getDiffsBySHAs = async (shas: SHA[]) => {
|
||||
return Promise.all(shas.map((sha) => getCommitDiff(sha)));
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Message improvement */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
async function improveMessagesInChunks(
|
||||
diffsAndSHAs: DiffAndSHA[]
|
||||
): Promise<MsgAndSHA[]> {
|
||||
const chunkSize = diffsAndSHAs.length % 2 === 0 ? 4 : 3;
|
||||
// send only 3-4 size chunks of diffs in steps,
|
||||
// because openAI restricts "too many requests" at once with 429 error
|
||||
async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
|
||||
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
||||
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
||||
|
||||
const improvePromises = diffsAndSHAs.map((commit) =>
|
||||
const improvePromises = diffsAndSHAs!.map((commit) =>
|
||||
generateCommitMessageByDiff(commit.diff, false)
|
||||
);
|
||||
|
||||
const improved: MsgAndSHA[] = [];
|
||||
|
||||
let improvedMessagesAndSHAs: MsgAndSHA[] = [];
|
||||
for (let step = 0; step < improvePromises.length; step += chunkSize) {
|
||||
const chunk = improvePromises.slice(step, step + chunkSize);
|
||||
|
||||
const chunkOfPromises = improvePromises.slice(step, step + chunkSize);
|
||||
try {
|
||||
const results = await Promise.all(chunk);
|
||||
|
||||
results.forEach((msg, i) => {
|
||||
improved.push({
|
||||
sha: diffsAndSHAs[step + i].sha,
|
||||
msg,
|
||||
});
|
||||
});
|
||||
|
||||
const chunkOfImprovedMessages = await Promise.all(chunkOfPromises);
|
||||
const chunkOfImprovedMessagesBySha = chunkOfImprovedMessages.map(
|
||||
(improvedMsg, i) => {
|
||||
const index = improvedMessagesAndSHAs.length;
|
||||
const sha = diffsAndSHAs![index + i].sha;
|
||||
return { sha, msg: improvedMsg };
|
||||
}
|
||||
);
|
||||
improvedMessagesAndSHAs.push(...chunkOfImprovedMessagesBySha);
|
||||
// sometimes openAI errors with 429 code (too many requests),
|
||||
// so lets sleep a bit
|
||||
const sleepFor =
|
||||
1000 * randomIntFromInterval(1, 5) +
|
||||
100 * randomIntFromInterval(1, 5);
|
||||
|
||||
1000 * randomIntFromInterval(1, 5) + 100 * randomIntFromInterval(1, 5);
|
||||
outro(
|
||||
`Improved ${chunkOfPromises.length} messages. Sleeping for ${sleepFor}`
|
||||
);
|
||||
await sleep(sleepFor);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
outro(error as string);
|
||||
// if sleeping in try block still fails with 429,
|
||||
// openAI wants at least 1 minute before next request
|
||||
const sleepFor = 60000 + 1000 * randomIntFromInterval(1, 5);
|
||||
outro(`Retrying after sleeping for ${sleepFor}`);
|
||||
await sleep(sleepFor);
|
||||
// go to previous step
|
||||
step -= chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
return improved;
|
||||
return improvedMessagesAndSHAs;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Main logic */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const getDiffsBySHAs = async (SHAs: string[]) => {
|
||||
const diffPromises = SHAs.map((sha) => getCommitDiff(sha));
|
||||
const diffs = await Promise.all(diffPromises).catch((error) => {
|
||||
outro(`Error in Promise.all(getCommitDiffs(SHAs)): ${error}.`);
|
||||
throw error;
|
||||
});
|
||||
return diffs;
|
||||
};
|
||||
|
||||
async function improveCommitMessages(
|
||||
commits: { id: string; message: string }[]
|
||||
commitsToImprove: { id: string; message: string }[]
|
||||
): Promise<void> {
|
||||
const eligibleCommits = commits.filter(
|
||||
(c) => !shouldSkipCommit(c.message)
|
||||
);
|
||||
|
||||
if (!eligibleCommits.length) {
|
||||
outro('No eligible commits found.');
|
||||
if (commitsToImprove.length) {
|
||||
outro(`Found ${commitsToImprove.length} commits to improve.`);
|
||||
} else {
|
||||
outro('No new commits found.');
|
||||
return;
|
||||
}
|
||||
|
||||
outro(`Found ${eligibleCommits.length} commits to improve.`);
|
||||
|
||||
const commitSHAs = eligibleCommits.map((c) => c.id);
|
||||
const diffsWithSHAs = await getDiffsBySHAs(commitSHAs);
|
||||
|
||||
const improvedMessages = await improveMessagesInChunks(diffsWithSHAs);
|
||||
|
||||
const hasChanges = improvedMessages.some(
|
||||
(improved, i) => improved.msg !== eligibleCommits[i].message
|
||||
);
|
||||
|
||||
if (!hasChanges) {
|
||||
outro('No changes in commit messages detected.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
outro('Dry run enabled. No commits will be rewritten.');
|
||||
improvedMessages.forEach((c) =>
|
||||
console.log(`[DRY RUN] ${c.sha}: ${c.msg}`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
improvedMessages.forEach(({ msg }, i) =>
|
||||
writeFileSync(`./commit-${i}.txt`, msg)
|
||||
);
|
||||
|
||||
writeFileSync('./count.txt', '0');
|
||||
|
||||
writeFileSync(
|
||||
'./rebase-exec.sh',
|
||||
`#!/bin/bash
|
||||
count=$(cat count.txt)
|
||||
git commit --amend -F commit-$count.txt
|
||||
echo $((count + 1)) > count.txt`
|
||||
);
|
||||
|
||||
await exec.exec('chmod +x ./rebase-exec.sh');
|
||||
|
||||
await exec.exec(
|
||||
'git',
|
||||
['rebase', `${eligibleCommits[0].id}^`, '--exec', './rebase-exec.sh'],
|
||||
{
|
||||
env: {
|
||||
GIT_SEQUENCE_EDITOR: 'sed -i -e "s/^pick/reword/g"',
|
||||
GIT_COMMITTER_NAME: process.env.GITHUB_ACTOR!,
|
||||
GIT_COMMITTER_EMAIL: `${process.env.GITHUB_ACTOR}@users.noreply.github.com`,
|
||||
},
|
||||
// Filter out merge commits and commits that have already been processed
|
||||
const filteredCommits = commitsToImprove.filter(commit => {
|
||||
// Skip merge commits
|
||||
if (commit.message.includes('Merge branch')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip commits that already have improved messages (simple heuristic)
|
||||
if (commit.message.includes('[OpenCommit]') || commit.message.includes('[auto]')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredCommits.length === 0) {
|
||||
outro('No commits to improve after filtering.');
|
||||
return;
|
||||
}
|
||||
|
||||
outro('Fetching commit diffs by SHAs.');
|
||||
const commitSHAsToImprove = filteredCommits.map((commit) => commit.id);
|
||||
const diffsWithSHAs = await getDiffsBySHAs(commitSHAsToImprove);
|
||||
outro('Done.');
|
||||
|
||||
const improvedMessagesWithSHAs = await improveMessagesInChunks(diffsWithSHAs);
|
||||
console.log(
|
||||
`Improved ${improvedMessagesWithSHAs.length} commit messages`
|
||||
);
|
||||
|
||||
improvedMessages.forEach((_, i) =>
|
||||
unlinkSync(`./commit-${i}.txt`)
|
||||
);
|
||||
// Only process commits if not in dry run mode
|
||||
if (DRY_RUN) {
|
||||
outro('DRY_RUN mode enabled. Skipping actual commit changes.');
|
||||
return;
|
||||
}
|
||||
|
||||
unlinkSync('./count.txt');
|
||||
unlinkSync('./rebase-exec.sh');
|
||||
|
||||
outro('Force pushing rewritten commits.');
|
||||
await exec.exec('git', ['push', '--force']);
|
||||
// Apply the improved commit messages
|
||||
try {
|
||||
// This would be where we actually modify commits
|
||||
// For now, we'll just log what would happen
|
||||
outro('Would apply improved commit messages (DRY_RUN mode disabled).');
|
||||
// In a real implementation, this would:
|
||||
// 1. Create new commits with improved messages
|
||||
// 2. Push them to the repository
|
||||
// 3. Handle the rebase or replacement logic
|
||||
} catch (error) {
|
||||
core.error(`Failed to apply improved commit messages: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Entrypoint */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
async function run() {
|
||||
intro('OpenCommit — improving commit messages');
|
||||
|
||||
try {
|
||||
if (context.eventName !== 'push') {
|
||||
if (PLATFORM === 'gitea' || PLATFORM === 'forgejo') {
|
||||
outro(`Running on ${PLATFORM} platform`);
|
||||
} else {
|
||||
outro('Running on GitHub platform');
|
||||
}
|
||||
|
||||
if (github.context.eventName === 'push') {
|
||||
const commits = github.context.payload.commits || [];
|
||||
|
||||
// Skip if in dry run mode
|
||||
if (DRY_RUN) {
|
||||
outro('DRY_RUN mode enabled. Skipping commit processing.');
|
||||
return;
|
||||
}
|
||||
|
||||
await improveCommitMessages(commits);
|
||||
} else {
|
||||
outro('Wrong action.');
|
||||
core.error(
|
||||
`OpenCommit is only supported on push events (got ${context.eventName})`
|
||||
`OpenCommit was called on ${github.context.payload.action}. OpenCommit is supposed to be used on "push" action.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = context.payload as PushEvent;
|
||||
|
||||
await exec.exec('git', [
|
||||
'config',
|
||||
'user.name',
|
||||
payload.pusher.name,
|
||||
]);
|
||||
|
||||
if (payload.pusher.email) {
|
||||
await exec.exec('git', [
|
||||
'config',
|
||||
'user.email',
|
||||
payload.pusher.email,
|
||||
]);
|
||||
}
|
||||
|
||||
await improveCommitMessages(payload.commits);
|
||||
} catch (error: any) {
|
||||
core.setFailed(error?.message || error);
|
||||
const err = error?.message || error;
|
||||
core.setFailed(err);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
run();
|
||||
@@ -1,96 +0,0 @@
|
||||
// src/platform.ts
|
||||
import { Gitea } from '@go-gitea/sdk.js';
|
||||
import github from '@actions/github';
|
||||
import { PushEvent } from '@octokit/webhooks-types';
|
||||
|
||||
export const REWRITE_MARKER = '[OPENCOMMIT]';
|
||||
|
||||
export type NormalizedCommit = {
|
||||
id: string;
|
||||
message: string;
|
||||
author?: { name?: string; email?: string };
|
||||
};
|
||||
|
||||
export function isGitea(): boolean {
|
||||
return !!process.env.GITEA_BASE_URL;
|
||||
}
|
||||
|
||||
export async function getPushCommits(): Promise<NormalizedCommit[]> {
|
||||
if (isGitea()) {
|
||||
const payload = github.context.payload as any;
|
||||
const commits = payload.commits as Array<any>;
|
||||
|
||||
return commits
|
||||
.filter((commit) => {
|
||||
if (commit.message.startsWith('Merge')) return false;
|
||||
if (commit.message.includes(REWRITE_MARKER)) return false;
|
||||
return true;
|
||||
})
|
||||
.map((commit) => ({
|
||||
id: commit.id,
|
||||
message: commit.message,
|
||||
author: commit.author
|
||||
? {
|
||||
name: commit.author.name,
|
||||
email: commit.author.email ?? undefined, // coerce null → undefined
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
} else {
|
||||
const payload = github.context.payload as PushEvent;
|
||||
const commits = payload.commits;
|
||||
|
||||
return commits
|
||||
.filter((commit) => {
|
||||
if (commit.message.startsWith('Merge')) return false;
|
||||
if (commit.message.includes(REWRITE_MARKER)) return false;
|
||||
return true;
|
||||
})
|
||||
.map((commit) => ({
|
||||
id: commit.id,
|
||||
message: commit.message,
|
||||
author: {
|
||||
name: commit.author.name,
|
||||
email: commit.author.email ?? undefined, // coerce null → undefined
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCommitDiff(
|
||||
sha: string,
|
||||
giteaConfig?: { baseUrl: string; token: string }
|
||||
): Promise<{ sha: string; diff: string }> {
|
||||
if (isGitea() && giteaConfig) {
|
||||
const gitea = new Gitea({
|
||||
baseUrl: giteaConfig.baseUrl,
|
||||
auth: giteaConfig.token,
|
||||
});
|
||||
|
||||
const { data } = await gitea.rest.git.getCommit({
|
||||
owner: process.env.GITEA_REPO_OWNER!,
|
||||
repo: process.env.GITEA_REPO_NAME!,
|
||||
sha,
|
||||
});
|
||||
|
||||
const filesChanged =
|
||||
data.files?.map((f: any) => `diff --git a/${f.filename} b/${f.filename}`).join('\n') ?? '';
|
||||
|
||||
return { sha, diff: filesChanged };
|
||||
} else {
|
||||
const octokit = github.getOctokit(process.env.GITHUB_TOKEN!);
|
||||
const context = github.context;
|
||||
|
||||
const { data: diff } = await octokit.request<string>(
|
||||
'GET /repos/{owner}/{repo}/commits/{ref}',
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: sha,
|
||||
headers: { Accept: 'application/vnd.github.v3.diff' },
|
||||
}
|
||||
);
|
||||
|
||||
return { sha, diff };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user