From a2c0b16a148ee568078d14603c2a8ec3c6c1c96b Mon Sep 17 00:00:00 2001 From: "Trez.One" Date: Sat, 3 Jan 2026 17:51:07 -0500 Subject: [PATCH] Gitea and Forgejo compatibility. --- src/github-action.ts | 317 +++++++++++++++++++++++++++---------------- 1 file changed, 199 insertions(+), 118 deletions(-) diff --git a/src/github-action.ts b/src/github-action.ts index a8e52f0..dca61cf 100644 --- a/src/github-action.ts +++ b/src/github-action.ts @@ -4,199 +4,280 @@ 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 { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff'; import { randomIntFromInterval } from './utils/randomIntFromInterval'; import { sleep } from './utils/sleep'; -// ------------ New Imports ------------ -import fetch from 'node-fetch'; - -// 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; +/* -------------------------------------------------------------------------- */ +/* Types */ +/* -------------------------------------------------------------------------- */ type SHA = string; type Diff = string; -async function fetchDiffFromAPI(commitSha: string) { - if (PLATFORM === 'github') { - const resp = await octokit.request( - '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 { sha: SHA; diff: Diff; } + interface MsgAndSHA { sha: SHA; msg: string; } -// split into chunks for OpenAI -async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) { - const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3; +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( + '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 { + const chunkSize = diffsAndSHAs.length % 2 === 0 ? 4 : 3; 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) { - const chunkPromises = improvePromises.slice(step, step + chunkSize); + const chunk = improvePromises.slice(step, step + chunkSize); + try { - const results = await Promise.all(chunkPromises); - results.forEach((msg, idx) => { - const sha = diffsAndSHAs![improved.length + idx].sha; - improved.push({ sha, msg }); + const results = await Promise.all(chunk); + + results.forEach((msg, i) => { + improved.push({ + sha: diffsAndSHAs[step + i].sha, + msg, + }); }); - await sleep( + + const sleepFor = 1000 * randomIntFromInterval(1, 5) + - 100 * randomIntFromInterval(1, 5) - ); - } catch (e) { - const wait = 60000 + 1000 * randomIntFromInterval(1, 5); - outro(`Retrying after ${wait}`); - await sleep(wait); + 100 * randomIntFromInterval(1, 5); + + await sleep(sleepFor); + } catch (err) { + const sleepFor = 60000 + 1000 * randomIntFromInterval(1, 5); + await sleep(sleepFor); step -= chunkSize; } } + return improved; } +/* -------------------------------------------------------------------------- */ +/* Main logic */ +/* -------------------------------------------------------------------------- */ + async function improveCommitMessages( commits: { id: string; message: string }[] -) { - const toImprove = filterCommits(commits); - if (!toImprove.length) { - 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 +): Promise { + const eligibleCommits = commits.filter( + (c) => !shouldSkipCommit(c.message) ); - if (!changesExist) { - outro('No commit message changes.'); + if (!eligibleCommits.length) { + outro('No eligible commits found.'); return; } - if (DRY_RUN) { - outro('Dry‑run: logging proposed changes (no rebase/push).'); - improved.forEach(({ sha, msg }) => - console.log(`Would rewrite ${sha} => ${msg}`) + 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; } - improved.forEach(({ msg }, i) => + 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` +echo $((count + 1)) > count.txt` ); await exec.exec('chmod +x ./rebase-exec.sh'); + await exec.exec( 'git', - ['rebase', `${shas[0]}^`, '--exec', './rebase-exec.sh'], + ['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` - } + 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('./rebase-exec.sh'); + outro('Force pushing rewritten commits.'); await exec.exec('git', ['push', '--force']); - outro('Done'); } +/* -------------------------------------------------------------------------- */ +/* Entrypoint */ +/* -------------------------------------------------------------------------- */ + async function run() { intro('OpenCommit — improving commit messages'); + try { - 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 { + if (context.eventName !== 'push') { 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) { - core.setFailed(error.message || String(error)); + core.setFailed(error?.message || error); } }