Gitea and Forgejo compatibility.
This commit is contained in:
+199
-118
@@ -4,199 +4,280 @@ import github from '@actions/github';
|
|||||||
import { intro, outro } from '@clack/prompts';
|
import { intro, outro } from '@clack/prompts';
|
||||||
import { PushEvent } from '@octokit/webhooks-types';
|
import { PushEvent } from '@octokit/webhooks-types';
|
||||||
import { unlinkSync, writeFileSync } from 'fs';
|
import { unlinkSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
|
import { Gitea } from '@go-gitea/sdk.js';
|
||||||
|
|
||||||
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff';
|
||||||
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
||||||
import { sleep } from './utils/sleep';
|
import { sleep } from './utils/sleep';
|
||||||
|
|
||||||
// ------------ New Imports ------------
|
/* -------------------------------------------------------------------------- */
|
||||||
import fetch from 'node-fetch';
|
/* Types */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
// Inputs
|
|
||||||
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN');
|
|
||||||
const DRY_RUN = core.getInput('DRY_RUN') === 'true';
|
|
||||||
const ocPrefix = '[OpenCommit]'; // Commit prefix to skip
|
|
||||||
|
|
||||||
// Platforms: github | gitea | forgejo (via GIT_PLATFORM env)
|
|
||||||
const PLATFORM = (process.env.GIT_PLATFORM || 'github').toLowerCase();
|
|
||||||
|
|
||||||
const octokit = github.getOctokit(GITHUB_TOKEN, {
|
|
||||||
log: {
|
|
||||||
debug: (message) => console.log('[octokit debug]', message),
|
|
||||||
info: (message) => console.log('[octokit info]', message),
|
|
||||||
warn: (message) => console.warn('[octokit warn]', message),
|
|
||||||
error: (message) => console.error('[octokit error]', message),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const context = github.context;
|
|
||||||
const owner = context.repo.owner;
|
|
||||||
const repo = context.repo.repo;
|
|
||||||
|
|
||||||
type SHA = string;
|
type SHA = string;
|
||||||
type Diff = string;
|
type Diff = string;
|
||||||
|
|
||||||
async function fetchDiffFromAPI(commitSha: string) {
|
|
||||||
if (PLATFORM === 'github') {
|
|
||||||
const resp = await octokit.request<string>(
|
|
||||||
'GET /repos/{owner}/{repo}/commits/{ref}',
|
|
||||||
{
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
ref: commitSha,
|
|
||||||
headers: { Accept: 'application/vnd.github.v3.diff' }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return resp.data;
|
|
||||||
} else {
|
|
||||||
// Gitea/Forgejo unified diff endpoint
|
|
||||||
const diffUrl = `${process.env.GIT_PLATFORM_API_BASE}/repos/${owner}/${repo}/git/commits/${commitSha}.diff`;
|
|
||||||
const response = await fetch(diffUrl, {
|
|
||||||
headers: { Authorization: `token ${GITHUB_TOKEN}` }
|
|
||||||
});
|
|
||||||
return response.text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCommitDiff(commitSha: string) {
|
|
||||||
const diff = await fetchDiffFromAPI(commitSha);
|
|
||||||
return { sha: commitSha, diff };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out merge commits and those already rewritten
|
|
||||||
function filterCommits(
|
|
||||||
commits: { id: string; message: string }[]
|
|
||||||
) {
|
|
||||||
return commits.filter(({ message }) => {
|
|
||||||
if (message.startsWith('Merge')) return false;
|
|
||||||
if (message.startsWith(ocPrefix)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DiffAndSHA {
|
interface DiffAndSHA {
|
||||||
sha: SHA;
|
sha: SHA;
|
||||||
diff: Diff;
|
diff: Diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MsgAndSHA {
|
interface MsgAndSHA {
|
||||||
sha: SHA;
|
sha: SHA;
|
||||||
msg: string;
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// split into chunks for OpenAI
|
type Platform = 'github' | 'gitea';
|
||||||
async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
|
|
||||||
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* 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;
|
||||||
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
||||||
const improvePromises = diffsAndSHAs!.map((c) =>
|
|
||||||
generateCommitMessageByDiff(c.diff, false)
|
const improvePromises = diffsAndSHAs.map((commit) =>
|
||||||
|
generateCommitMessageByDiff(commit.diff, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
let improved: MsgAndSHA[] = [];
|
const improved: MsgAndSHA[] = [];
|
||||||
|
|
||||||
for (let step = 0; step < improvePromises.length; step += chunkSize) {
|
for (let step = 0; step < improvePromises.length; step += chunkSize) {
|
||||||
const chunkPromises = improvePromises.slice(step, step + chunkSize);
|
const chunk = improvePromises.slice(step, step + chunkSize);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(chunkPromises);
|
const results = await Promise.all(chunk);
|
||||||
results.forEach((msg, idx) => {
|
|
||||||
const sha = diffsAndSHAs![improved.length + idx].sha;
|
results.forEach((msg, i) => {
|
||||||
improved.push({ sha, msg });
|
improved.push({
|
||||||
|
sha: diffsAndSHAs[step + i].sha,
|
||||||
|
msg,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await sleep(
|
|
||||||
|
const sleepFor =
|
||||||
1000 * randomIntFromInterval(1, 5) +
|
1000 * randomIntFromInterval(1, 5) +
|
||||||
100 * randomIntFromInterval(1, 5)
|
100 * randomIntFromInterval(1, 5);
|
||||||
);
|
|
||||||
} catch (e) {
|
await sleep(sleepFor);
|
||||||
const wait = 60000 + 1000 * randomIntFromInterval(1, 5);
|
} catch (err) {
|
||||||
outro(`Retrying after ${wait}`);
|
const sleepFor = 60000 + 1000 * randomIntFromInterval(1, 5);
|
||||||
await sleep(wait);
|
await sleep(sleepFor);
|
||||||
step -= chunkSize;
|
step -= chunkSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return improved;
|
return improved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Main logic */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
async function improveCommitMessages(
|
async function improveCommitMessages(
|
||||||
commits: { id: string; message: string }[]
|
commits: { id: string; message: string }[]
|
||||||
) {
|
): Promise<void> {
|
||||||
const toImprove = filterCommits(commits);
|
const eligibleCommits = commits.filter(
|
||||||
if (!toImprove.length) {
|
(c) => !shouldSkipCommit(c.message)
|
||||||
outro('No eligible commits to improve.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shas = toImprove.map((c) => c.id);
|
|
||||||
const diffs = await Promise.all(shas.map((sha) => getCommitDiff(sha)));
|
|
||||||
|
|
||||||
const improved = await improveMessagesInChunks(diffs);
|
|
||||||
|
|
||||||
const changesExist = improved.some(
|
|
||||||
({ msg }, i) => msg !== toImprove[i].message
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!changesExist) {
|
if (!eligibleCommits.length) {
|
||||||
outro('No commit message changes.');
|
outro('No eligible commits found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DRY_RUN) {
|
outro(`Found ${eligibleCommits.length} commits to improve.`);
|
||||||
outro('Dry‑run: logging proposed changes (no rebase/push).');
|
|
||||||
improved.forEach(({ sha, msg }) =>
|
const commitSHAs = eligibleCommits.map((c) => c.id);
|
||||||
console.log(`Would rewrite ${sha} => ${msg}`)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
improved.forEach(({ msg }, i) =>
|
improvedMessages.forEach(({ msg }, i) =>
|
||||||
writeFileSync(`./commit-${i}.txt`, msg)
|
writeFileSync(`./commit-${i}.txt`, msg)
|
||||||
);
|
);
|
||||||
|
|
||||||
writeFileSync('./count.txt', '0');
|
writeFileSync('./count.txt', '0');
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
'./rebase-exec.sh',
|
'./rebase-exec.sh',
|
||||||
`#!/bin/bash
|
`#!/bin/bash
|
||||||
count=$(cat count.txt)
|
count=$(cat count.txt)
|
||||||
git commit --amend -F commit-$count.txt
|
git commit --amend -F commit-$count.txt
|
||||||
echo $((count+1)) > count.txt`
|
echo $((count + 1)) > count.txt`
|
||||||
);
|
);
|
||||||
|
|
||||||
await exec.exec('chmod +x ./rebase-exec.sh');
|
await exec.exec('chmod +x ./rebase-exec.sh');
|
||||||
|
|
||||||
await exec.exec(
|
await exec.exec(
|
||||||
'git',
|
'git',
|
||||||
['rebase', `${shas[0]}^`, '--exec', './rebase-exec.sh'],
|
['rebase', `${eligibleCommits[0].id}^`, '--exec', './rebase-exec.sh'],
|
||||||
{
|
{
|
||||||
env: {
|
env: {
|
||||||
GIT_SEQUENCE_EDITOR: 'sed -i -e "s/^pick/reword/g"',
|
GIT_SEQUENCE_EDITOR: 'sed -i -e "s/^pick/reword/g"',
|
||||||
GIT_COMMITTER_NAME: process.env.GITHUB_ACTOR!,
|
GIT_COMMITTER_NAME: process.env.GITHUB_ACTOR!,
|
||||||
GIT_COMMITTER_EMAIL: `${process.env.GITHUB_ACTOR}@users.noreply.github.com`
|
GIT_COMMITTER_EMAIL: `${process.env.GITHUB_ACTOR}@users.noreply.github.com`,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
toImprove.forEach((_c, i) => unlinkSync(`./commit-${i}.txt`));
|
improvedMessages.forEach((_, i) =>
|
||||||
|
unlinkSync(`./commit-${i}.txt`)
|
||||||
|
);
|
||||||
|
|
||||||
unlinkSync('./count.txt');
|
unlinkSync('./count.txt');
|
||||||
unlinkSync('./rebase-exec.sh');
|
unlinkSync('./rebase-exec.sh');
|
||||||
|
|
||||||
|
outro('Force pushing rewritten commits.');
|
||||||
await exec.exec('git', ['push', '--force']);
|
await exec.exec('git', ['push', '--force']);
|
||||||
outro('Done');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Entrypoint */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
intro('OpenCommit — improving commit messages');
|
intro('OpenCommit — improving commit messages');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (context.eventName === 'push') {
|
if (context.eventName !== 'push') {
|
||||||
const payload = context.payload as PushEvent;
|
|
||||||
const pusherEmail = payload.pusher.email ?? `${context.actor}@users.noreply.github.com`;
|
|
||||||
const pusherName = payload.pusher.name ?? context.actor;
|
|
||||||
|
|
||||||
await exec.exec('git', ['config', 'user.email', pusherEmail]);
|
|
||||||
await exec.exec('git', ['config', 'user.name', pusherName]);
|
|
||||||
|
|
||||||
|
|
||||||
await improveCommitMessages(payload.commits);
|
|
||||||
} else {
|
|
||||||
core.error(
|
core.error(
|
||||||
`Wrong event: expected push, got ${context.eventName}`
|
`OpenCommit is only supported on push events (got ${context.eventName})`
|
||||||
);
|
);
|
||||||
|
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) {
|
} catch (error: any) {
|
||||||
core.setFailed(error.message || String(error));
|
core.setFailed(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user