From 549ee31eaa5fd1295a91b0bf7087987c398491c9 Mon Sep 17 00:00:00 2001 From: Ankio Date: Tue, 31 Oct 2023 22:46:26 +0800 Subject: [PATCH] feat:add gitea platform resolver --- package-lock.json | 53 +++++++ package.json | 2 + src/main.ts | 24 ++- src/pr-collector/gitHelper.ts | 2 +- src/releaseNotesBuilder.ts | 2 +- src/repositories/GiteaRepository.ts | 231 +++++++++++++++++++++++++--- 6 files changed, 279 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index f767ba6..034e518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@actions/exec": "^1.1.1", "@actions/github": "^6.0.0", "@octokit/rest": "^20.0.2", + "cross-fetch": "^4.0.0", + "gitea-js": "^1.20.1", "https-proxy-agent": "^7.0.2", "moment": "^2.29.4", "semver": "^7.5.4" @@ -2555,6 +2557,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3924,6 +3934,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gitea-js": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/gitea-js/-/gitea-js-1.20.1.tgz", + "integrity": "sha512-g1wdNP5zYN258tTVIm2vGY8bRJTwVwCfYH0ockUpi34RAuOwJ0FavpFVVvazxXEcdo2XmXRKZcAmVhAhwMXkgg==" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5496,6 +5511,25 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6584,6 +6618,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -6934,6 +6973,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index a83cbdc..aa6c6fe 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "@actions/exec": "^1.1.1", "@actions/github": "^6.0.0", "@octokit/rest": "^20.0.2", + "cross-fetch": "^4.0.0", + "gitea-js": "^1.20.1", "https-proxy-agent": "^7.0.2", "moment": "^2.29.4", "semver": "^7.5.4" diff --git a/src/main.ts b/src/main.ts index d013fa4..e15803e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,31 +3,28 @@ import * as github from '@actions/github' import {mergeConfiguration, parseConfiguration, resolveConfiguration, retrieveRepositoryPath, writeOutput} from './utils' import {ReleaseNotesBuilder} from './releaseNotesBuilder' import {Configuration} from './configuration' -import {GithubRepository} from "./repositories/GithubRepository"; -import {GiteaRepository} from "./repositories/GiteaRepository"; +import {GithubRepository} from './repositories/GithubRepository' +import {GiteaRepository} from './repositories/GiteaRepository' async function run(): Promise { - const supportedPlatform = { github: GithubRepository, - gitea: GiteaRepository, - }; + gitea: GiteaRepository + } function isSupportedPlatform(type: string): type is keyof typeof supportedPlatform { - return type in supportedPlatform; + return type in supportedPlatform } core.setOutput('failed', false) // mark the action not failed by default core.startGroup(`📘 Reading input values`) try { // read in path specification, resolve github workspace, and repo path - const platform = core.getInput('platform') || "github" - if(!isSupportedPlatform(platform)){ + const platform = core.getInput('platform') || 'github' + if (!isSupportedPlatform(platform)) { core.setFailed(`The ${platform} platform is not supported. `) return } - - const inputPath = core.getInput('path') const repositoryPath = retrieveRepositoryPath(inputPath) @@ -58,7 +55,7 @@ async function run(): Promise { // read in repository inputs const baseUrl = core.getInput('baseUrl') - const token = core.getInput('token') || process.env.GITHUB_TOKEN || "" + const token = core.getInput('token') || process.env.GITHUB_TOKEN || '' const owner = core.getInput('owner') || github.context.repo.owner const repo = core.getInput('repo') || github.context.repo.repo // read in from, to tag inputs @@ -77,11 +74,10 @@ async function run(): Promise { const exportOnly = core.getInput('exportOnly') === 'true' const cache = core.getInput('cache') - - const repositoryUtils = new supportedPlatform[platform](token,baseUrl); + const repositoryUtils = new supportedPlatform[platform](token, baseUrl, repositoryPath) const result = await new ReleaseNotesBuilder( baseUrl, - repositoryUtils, + repositoryUtils, repositoryPath, owner, repo, diff --git a/src/pr-collector/gitHelper.ts b/src/pr-collector/gitHelper.ts index 59b0d4e..035ebd8 100644 --- a/src/pr-collector/gitHelper.ts +++ b/src/pr-collector/gitHelper.ts @@ -39,7 +39,7 @@ class GitCommandManager { return result } - private async execGit(args: string[], allowAllExitCodes = false, silent = false): Promise { + async execGit(args: string[], allowAllExitCodes = false, silent = false): Promise { directoryExistsSync(this.workingDirectory, true) const result = new GitOutput() diff --git a/src/releaseNotesBuilder.ts b/src/releaseNotesBuilder.ts index 808053c..535608c 100644 --- a/src/releaseNotesBuilder.ts +++ b/src/releaseNotesBuilder.ts @@ -8,7 +8,7 @@ import {TagInfo} from './pr-collector/tags' import {DiffInfo} from './pr-collector/commits' import {PullRequestInfo} from './pr-collector/pullRequests' import * as fs from 'fs' -import {BaseRepository} from "./repositories/BaseRepository"; +import {BaseRepository} from './repositories/BaseRepository' export interface ReleaseNotesOptions { owner: string // the owner of the repository diff --git a/src/repositories/GiteaRepository.ts b/src/repositories/GiteaRepository.ts index 20fda33..7cda424 100644 --- a/src/repositories/GiteaRepository.ts +++ b/src/repositories/GiteaRepository.ts @@ -1,43 +1,236 @@ import {BaseRepository} from "./BaseRepository"; import {TagInfo} from "../pr-collector/tags"; -import {PullRequestInfo} from "../pr-collector/pullRequests"; +import { + CommentInfo, + PullRequestInfo, +} from "../pr-collector/pullRequests"; import {DiffInfo} from "../pr-collector/commits"; +import {Api, PullRequest, PullReview} from "gitea-js"; +import moment from "moment"; +import * as core from "@actions/core"; +import {createCommandManager} from "../pr-collector/gitHelper"; + +export class GiteaRepository extends BaseRepository { + private api: Api -export class GiteaRepository extends BaseRepository{ get defaultUrl(): string { - return "https://gitea.com/api/v1"; + return "https://gitea.com"; } - constructor(token: string, url?: string) { - super(token, url); + + constructor(token: string, url: string | undefined, repositoryPath: string) { + super(token, url, repositoryPath); this.url = url || this.defaultUrl + const fetch = require('cross-fetch'); + const {giteaApi} = require('gitea-js'); + this.api = new giteaApi('https://gitea.com/', { + token: token, // generate one at https://gitea.example.com/user/settings/applications + customFetch: fetch, + }); } - fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise { - return Promise.resolve(undefined); + async fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise { + const response = await this.api.repos.repoGetTag(owner, repo, tagInfo.name) + + if (response.error === null) { + + if (response.data.commit) { + tagInfo.date = moment(response.data.commit.created) + core.info(`ℹ️ Retrieved information about the release associated with ${tagInfo.name} from the Gitea API`) + return tagInfo + } + + + } + return await this.getTagByCreateTime(repositoryPath, tagInfo) + } - getBetweenDates(owner: string, repo: string, fromDate: moment.Moment, toDate: moment.Moment, maxPullRequests: number): Promise { - return Promise.resolve([]); + async getBetweenDates(owner: string, repo: string, fromDate: moment.Moment, toDate: moment.Moment, maxPullRequests: number): Promise { + + const mergedPRs: PullRequestInfo[] = [] + + const response = await this.api.repos.repoListPullRequests(owner, repo, { + sort: 'recentupdate', + state: 'closed', + limit: Math.min(100, maxPullRequests), + }) + + if (response.error === null) { + + for (const pr of response.data.filter(p => !!p.merged_at)) { + mergedPRs.push(this.mapPullRequest(pr, 'merged')) + } + } + + + return mergedPRs; } - getDiffRemote(owner: string, repo: string, base: string, head: string): Promise { - return Promise.resolve(undefined); + private mapPullRequest(pr: PullRequest, status: 'open' | 'merged' = 'open'): PullRequestInfo { + return { + number: pr.number || 0, + title: pr.title || "", + htmlURL: pr.html_url || "", + baseBranch: pr.base?.repo?.default_branch || "", + branch: pr.merge_base || "", + mergedAt: pr.merged_at ? moment(pr.merged_at) : undefined, + mergeCommitSha: pr.merge_commit_sha || "", + author: pr.user?.login || "", + repoName: pr.base?.repo?.full_name || "", + labels: pr.labels?.map((label) => label.name) as string[], + milestone: pr.milestone?.title || "", + body: pr.body || "", + + assignees: pr.assignees?.map((user) => user.full_name) as string[], + requestedReviewers: pr.requested_reviewers?.map((user) => user.full_name) as string[], + approvedReviewers: [], + createdAt: moment(pr.created_at), + status: status, + reviews: undefined + } } - getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise { - return Promise.resolve([]); + async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise { + let changedFilesCount = 0; + let additionCount = 0; + let deletionCount = 0; + let changeCount = 0; + let commitCount = 0; + + const gitHelper = await createCommandManager(this.repositoryPath) + + // Get the diff stats between the two branches/commits + const diffStat = await gitHelper.execGit(['diff', '--stat', `${base}...${head}`]); + const diffStatLines = diffStat.stdout.split('\n'); + + for (const line of diffStatLines) { + // Extract the addition and deletion counts from each line of the git diff output + const match = line.match(/(\d+) insertions?\(\+\), (\d+) deletions?\(\-\)/); + if (match) { + additionCount += parseInt(match[1], 10); + deletionCount += parseInt(match[2], 10); + } + } + + // Get the list of changed files + const diffNameOnly = await gitHelper.execGit(['diff', '--name-only', `${base}...${head}`]); + const changedFiles = diffNameOnly.stdout.split('\n'); + changedFilesCount = changedFiles.length - 1; // Subtract one for the empty line at the end + + // Get the commit count between the two branches/commits + const logCount = await gitHelper.execGit(['rev-list', '--count', `${base}...${head}`]); + commitCount = parseInt(logCount.stdout.trim(), 10); + + // Now let's get the commit logs between the two branches/commits + const log = await gitHelper.execGit(['log', '--pretty=format:%H||||%an||||%ae||||%ad||||%cn||||%ce||||%cd||||%s', `${base}...${head}`]); + const commitLogs = log.stdout.trim().split('\n'); + + // Process commit logs + const commitInfo = commitLogs.map(log => { + const [sha, authorName, authorEmail, authorDate, committerName, committerEmail, committerDate, subject] = log.split('||||'); + return { + sha, + summary: subject, + message: '', // This would require another git command to get the full message if needed + author: authorName, + authorDate: moment(authorDate, "ddd MMM DD HH:mm:ss YYYY ZZ", false), + committer: committerName, + commitDate: moment(committerDate, "ddd MMM DD HH:mm:ss YYYY ZZ", false), + prNumber: undefined // This is not available directly from git, would require additional logic to associate commits with PRs + }; + }); + + return { + changedFiles: changedFilesCount, + additions: additionCount, + deletions: deletionCount, + changes: changeCount, + commits: commitCount, + commitInfo: commitInfo + }; } - getOpen(owner: string, repo: string, maxPullRequests: number): Promise { - return Promise.resolve([]); + + async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise { + const mergedPRs: PullRequestInfo[] = [] + + const response = await this.api.repos.repoListPullRequests(owner, repo, { + sort: 'recentupdate', + state: 'closed', + limit: 1000, + }) + + + + if (response.error === null) { + + for (const pr of response.data) { + if (pr.merge_commit_sha === commit_sha) { + mergedPRs.push(this.mapPullRequest(pr, pr.merged_at ? 'merged' : 'open')) + } + } + } + + return mergedPRs + + } - getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise { - return Promise.resolve(undefined); + async getOpen(owner: string, repo: string, maxPullRequests: number): Promise { + const openPrs: PullRequestInfo[] = [] + const response = await this.api.repos.repoListPullRequests(owner, repo, { + sort: 'recentupdate', + state: 'open', + limit: Math.min(100, maxPullRequests), + }) + if (response.error === null) { + + for (const pr of response.data) { + openPrs.push(this.mapPullRequest(pr, 'open')) + } + } + + return openPrs } - getTags(owner: string, repo: string, maxTagsToFetch: number): Promise { - return Promise.resolve([]); + async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise { + + const prReviews: CommentInfo[] = [] + const response = await this.api.repos.repoListPullReviews(owner, repo, pr.number, {}) + if (response.error === null) { + + for (const comment of response.data) { + prReviews.push(this.mapComment(comment)) + } + } + pr.reviews = prReviews + } + + private mapComment = (comment: PullReview): CommentInfo => ({ + id: comment.id || 0, + htmlURL: comment.html_url || "", + submittedAt: comment.submitted_at ? moment(comment.submitted_at) : undefined, + author: comment.user?.login || '', + body: comment.body || "", + state: comment.state + }) + + async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise { + const response = await this.api.repos.repoListTags(owner, repo, { + limit: maxTagsToFetch + }) + const tagsInfo: TagInfo[] = [] + if (response.error === null) { + for (const tag of response.data) { + tagsInfo.push({ + name: tag.name || "", + commit: tag.commit?.sha + }) + } + + } + core.info(`ℹ️ Found ${tagsInfo.length} (fetching max: ${maxTagsToFetch}) tags from the GitHub API for ${owner}/${repo}`) + return tagsInfo } } \ No newline at end of file