diff --git a/docs/plugins/development/git.md b/docs/plugins/development/git.md index 4e6b3602d..d5a6f3cbc 100644 --- a/docs/plugins/development/git.md +++ b/docs/plugins/development/git.md @@ -63,28 +63,7 @@ This plugin will significantly slow down the speed of data preparation, especial - Type: `boolean | ContributorsOptions` ```ts - interface ContributorsOptions { - /** - * Functions to transform contributors, e.g. remove duplicates ones and sort them. - * The input is the contributors collected by this plugin, and the output should be the transformed contributors. - */ - transform?: ( - contributors: GitContributor[], - ) => GitContributor[] | Promise - - /** - * List of contributors configurations - */ - list?: ContributorConfig[] - - /** - * Whether to add avatar in contributor information - * @default false - */ - avatar?: boolean - } - - interface ContributorConfig { + interface ContributorInfo { /** * Contributor's username on the git hosting service */ @@ -115,6 +94,25 @@ This plugin will significantly slow down the speed of data preparation, especial */ url?: string } + + interface ContributorsOptions { + /** + * Contributor information + */ + info?: ContributorInfo[] + + /** + * Whether to add avatar in contributor information + * @default false + */ + avatar?: boolean + + /** + * Functions to transform contributors, e.g. remove duplicates ones and sort them. + * The input is the contributors collected by this plugin, and the output should be the transformed contributors. + */ + transform?: (contributors: GitContributor[]) => GitContributor[] + } ``` - Default: `true` diff --git a/docs/zh/plugins/development/git.md b/docs/zh/plugins/development/git.md index 7474bb142..15f28e9b4 100644 --- a/docs/zh/plugins/development/git.md +++ b/docs/zh/plugins/development/git.md @@ -63,28 +63,7 @@ export default { - 类型: `boolean | ContributorsOptions` ```ts - interface ContributorsOptions { - /** - * 贡献者转换函数,例如去重和排序 - * 该函数接收一个贡献者信息数组,返回一个新的贡献者信息数组。 - */ - transform?: ( - contributors: GitContributor[], - ) => GitContributor[] | Promise - - /** - * 贡献者配置 - */ - list?: ContributorConfig[] - - /** - * 是否在贡献者信息中添加头像 - * @default false - */ - avatar?: boolean - } - - interface ContributorConfig { + interface ContributorInfo { /** * 贡献者在 git 托管服务中的用户名 */ @@ -109,6 +88,25 @@ export default { */ url?: string } + + interface ContributorsOptions { + /** + * 贡献者信息 + */ + info?: ContributorInfo[] + + /** + * 是否在贡献者信息中添加头像 + * @default false + */ + avatar?: boolean + + /** + * 贡献者转换函数,例如去重和排序 + * 该函数接收一个贡献者信息数组,返回一个新的贡献者信息数组。 + */ + transform?: (contributors: GitContributor[]) => GitContributor[] + } ``` - 默认值: `true` diff --git a/plugins/development/plugin-git/src/node/gitPlugin.ts b/plugins/development/plugin-git/src/node/gitPlugin.ts index 5955cecfe..ff82e6ea9 100644 --- a/plugins/development/plugin-git/src/node/gitPlugin.ts +++ b/plugins/development/plugin-git/src/node/gitPlugin.ts @@ -5,14 +5,10 @@ import type { GitPluginFrontmatter, GitPluginOptions, GitPluginPageData, -} from './types.js' -import { - checkGitRepo, - getCommits, - inferGitProvider, - resolveChangelog, - resolveContributors, -} from './utils/index.js' +} from './options.js' +import { resolveChangelog } from './resolveChangelog.js' +import { resolveContributors } from './resolveContributors.js' +import { checkGitRepo, getCommits, inferGitProvider } from './utils/index.js' export const gitPlugin = ({ @@ -74,13 +70,16 @@ export const gitPlugin = page.data.git.updatedTime = commits[0].date } + const contributorsOptions = isPlainObject(contributors) + ? contributors + : {} + if ((frontmatter.contributors ?? contributors) !== false) { - const options = isPlainObject(contributors) ? contributors : {} - options.transform ??= transformContributors - page.data.git.contributors = await resolveContributors( + contributorsOptions.transform ??= transformContributors + page.data.git.contributors = resolveContributors( commits, - options, gitProvider, + contributorsOptions, Array.isArray(frontmatter.contributors) ? frontmatter.contributors : [], @@ -89,15 +88,13 @@ export const gitPlugin = if (frontmatter.changelog ?? changelog) { const changelogOptions = isPlainObject(changelog) ? changelog : {} - const contributorsOptions = isPlainObject(contributors) - ? contributors - : {} + page.data.git.changelog = resolveChangelog( app, commits, changelogOptions, gitProvider, - contributorsOptions.list ?? [], + contributorsOptions.info ?? [], ) } }, diff --git a/plugins/development/plugin-git/src/node/index.ts b/plugins/development/plugin-git/src/node/index.ts index 95d5a1ede..b211e8e0a 100644 --- a/plugins/development/plugin-git/src/node/index.ts +++ b/plugins/development/plugin-git/src/node/index.ts @@ -1,3 +1,6 @@ export * from './gitPlugin.js' -export type * from './types.js' +export * from './resolveChangelog.js' +export * from './resolveContributors.js' export * from './utils/index.js' +export type * from './options.js' +export type * from './typings.js' diff --git a/plugins/development/plugin-git/src/node/types.ts b/plugins/development/plugin-git/src/node/options.ts similarity index 79% rename from plugins/development/plugin-git/src/node/types.ts rename to plugins/development/plugin-git/src/node/options.ts index 116e507ad..32ac94001 100644 --- a/plugins/development/plugin-git/src/node/types.ts +++ b/plugins/development/plugin-git/src/node/options.ts @@ -1,75 +1,10 @@ import type { Page, PageFrontmatter } from 'vuepress' +import type { GitChangelog, GitContributor } from './typings.js' -export interface GitPluginOptions { - /** - * Page filter, if it returns `true`, the page will collect git information. - * - * 页面过滤器,如果返回 `true` ,该页面将收集 git 信息 - */ - filter?: (page: Page) => boolean - /** - * Whether to get the created time of a page - * - * 是否收集页面创建时间 - */ - createdTime?: boolean - - /** - * Whether to get the updated time of a page - * - * 是否收集页面更新时间 - */ - updatedTime?: boolean - - /** - * Whether to get the contributors of a page - * - * 是否收集页面的贡献者 - */ - contributors?: ContributorsOptions | boolean - - /** - * Whether to get the changelog of a page - * - * 是否收集页面的变更历史记录 - */ - changelog?: ChangelogOptions | false - - /** - * @deprecated use `contributors.transform` instead - * Functions to transform contributors, e.g. remove duplicates ones and sort them - */ - transformContributors?: (contributors: GitContributor[]) => GitContributor[] -} - -export interface ContributorsOptions { - /** - * Functions to transform contributors, e.g. remove duplicates ones and sort them - * - * 贡献者转换函数,例如去重和排序 - */ - transform?: ( - contributors: GitContributor[], - ) => GitContributor[] | Promise - - /** - * The list of contributors configurations - * - * 贡献者配置 - */ - list?: ContributorConfig[] - - /** - * Whether to add avatar in contributor information - * - * 是否在贡献者信息中添加头像 - * - * @default false - */ - avatar?: boolean -} - -export interface ContributorConfig { +/** + * Contributor information + */ +export interface ContributorInfo { /** * Contributor's username on the git hosting service * @@ -94,6 +29,7 @@ export interface ContributorConfig { * 这时候可以通过别名映射到真实的用户名 */ alias?: string[] | string + /** * The avatar url of the contributor. * @@ -104,6 +40,7 @@ export interface ContributorConfig { * 如果 git 托管服务为 `github`,则可以忽略不填,由插件自动填充 */ avatar?: string + /** * The url of the contributor * @@ -116,6 +53,31 @@ export interface ContributorConfig { url?: string } +export interface ContributorsOptions { + /** + * Contributors Information + * + * 贡献者信息 + */ + info?: ContributorInfo[] + + /** + * Whether to add avatar in contributor information + * + * 是否在贡献者信息中添加头像 + * + * @default false + */ + avatar?: boolean + + /** + * Functions to transform contributors, e.g. remove duplicates ones and sort them + * + * 贡献者转换函数,例如去重和排序 + */ + transform?: (contributors: GitContributor[]) => GitContributor[] +} + export interface ChangelogOptions { /** * Maximum number of changelog @@ -178,6 +140,48 @@ export interface ChangelogOptions { tagUrlPattern?: string } +export interface GitPluginOptions { + /** + * Page filter, if it returns `true`, the page will collect git information. + * + * 页面过滤器,如果返回 `true` ,该页面将收集 git 信息 + */ + filter?: (page: Page) => boolean + /** + * Whether to get the created time of a page + * + * 是否收集页面创建时间 + */ + createdTime?: boolean + + /** + * Whether to get the updated time of a page + * + * 是否收集页面更新时间 + */ + updatedTime?: boolean + + /** + * Whether to get the contributors of a page + * + * 是否收集页面的贡献者 + */ + contributors?: ContributorsOptions | boolean + + /** + * Whether to get the changelog of a page + * + * 是否收集页面的变更历史记录 + */ + changelog?: ChangelogOptions | false + + /** + * @deprecated use `contributors.transform` instead + * Functions to transform contributors, e.g. remove duplicates ones and sort them + */ + transformContributors?: (contributors: GitContributor[]) => GitContributor[] +} + export interface GitPluginFrontmatter extends PageFrontmatter { gitInclude?: string[] @@ -220,70 +224,3 @@ export interface GitData { */ changelog?: GitChangelog[] } - -export interface GitContributor { - name: string - email: string - commits: number - avatar?: string - url?: string -} - -export type KnownGitProvider = 'bitbucket' | 'gitee' | 'github' | 'gitlab' - -export interface RawCommit { - filepath: string - /** - * Commit hash - */ - hash: string - /** - * Unix timestamp in milliseconds - */ - date: number - /** - * Commit message - */ - message: string - /** - * Commit message body - */ - body: string - /** - * Commit refs - */ - refs: string - /** - * Commit author name - */ - author: string - /** - * Commit author email - */ - email: string - - /** - * The co-authors of the commit - */ - coAuthors?: Pick[] -} - -export interface MergedRawCommit extends Omit { - filepaths: string[] -} - -export interface GitChangelog - extends Omit { - /** - * The url of the commit - */ - commitUrl?: string - /** - * release tag - */ - tag?: string - /** - * The url of the release tag - */ - tagUrl?: string -} diff --git a/plugins/development/plugin-git/src/node/utils/resolveChangelog.ts b/plugins/development/plugin-git/src/node/resolveChangelog.ts similarity index 91% rename from plugins/development/plugin-git/src/node/utils/resolveChangelog.ts rename to plugins/development/plugin-git/src/node/resolveChangelog.ts index 2045a2a35..874d1e20a 100644 --- a/plugins/development/plugin-git/src/node/utils/resolveChangelog.ts +++ b/plugins/development/plugin-git/src/node/resolveChangelog.ts @@ -1,15 +1,14 @@ import type { App } from 'vuepress' +import type { ChangelogOptions, ContributorInfo } from './options.js' import type { - ChangelogOptions, - ContributorConfig, GitChangelog, KnownGitProvider, MergedRawCommit, -} from '../types.js' +} from './typings.js' import { - getContributorWithConfig, + getContributorInfo, getUserNameWithNoreplyEmail, -} from './resolveContributors.js' +} from './utils/index.js' interface Pattern { issue?: string @@ -72,7 +71,7 @@ export const resolveChangelog = ( commits: MergedRawCommit[], options: ChangelogOptions, gitProvider: KnownGitProvider | null, - contributors: ContributorConfig[], + contributors: ContributorInfo[], ): GitChangelog[] => { const pattern = getPattern(options, gitProvider) const repo = options.repoUrl @@ -85,9 +84,9 @@ export const resolveChangelog = ( for (const commit of sliceCommits) { const { hash, message, date, author, email, refs, coAuthors } = commit const tag = parseTagName(refs) - const contributor = getContributorWithConfig( - contributors, + const contributor = getContributorInfo( getUserNameWithNoreplyEmail(email) ?? author, + contributors, ) const resolved: GitChangelog = { hash, @@ -97,7 +96,7 @@ export const resolveChangelog = ( message: app.markdown.renderInline(message), } - if (coAuthors) resolved.coAuthors = coAuthors + if (coAuthors.length) resolved.coAuthors = coAuthors if (pattern.issue && repo) { resolved.message = resolved.message.replace( diff --git a/plugins/development/plugin-git/src/node/resolveContributors.ts b/plugins/development/plugin-git/src/node/resolveContributors.ts new file mode 100644 index 000000000..05e8fe5b3 --- /dev/null +++ b/plugins/development/plugin-git/src/node/resolveContributors.ts @@ -0,0 +1,125 @@ +import type { ContributorsOptions } from './options.js' +import type { + GitContributor, + KnownGitProvider, + MergedRawCommit, +} from './typings.js' +import { + digestSHA256, + getContributorInfo, + getUserNameWithNoreplyEmail, +} from './utils/index.js' + +export const getRawContributors = ( + commits: MergedRawCommit[], + options: ContributorsOptions, + gitProvider: KnownGitProvider | null, +): GitContributor[] => { + const contributors = new Map() + + for (const commit of commits) { + const authors = [ + { name: commit.author, email: commit.email }, + ...commit.coAuthors, + ] + + for (const { name: author, email } of authors) { + const config = getContributorInfo( + getUserNameWithNoreplyEmail(email) ?? author, + options.info, + ) + const username = config?.username ?? author + const name = config?.name ?? username + + const contributor = contributors.get(name + email) + + if (contributor) { + contributor.commits++ + } else { + const item: GitContributor = { + name, + email, + commits: 1, + } + + if (options.avatar) + item.avatar = + config?.avatar ?? + (gitProvider === 'github' + ? `https://avatars.githubusercontent.com/${username}?v=4` + : `https://gravatar.com/avatar/${digestSHA256(email || username)}?d=retro`) + + const url = + (config?.url ?? gitProvider === 'github') + ? `https://github.com/${username}` + : undefined + if (url) item.url = url + + contributors.set(name + email, item) + } + } + } + + return Array.from(contributors.values()).filter((item, index, self) => { + // If one of the contributors is a "noreply" email address, and there's + // already a contributor with the same name, it is very likely a duplicate, + // so it can be removed. + if (item.email.split('@')[1]?.match(/no-?reply/)) { + const realIndex = self.findIndex((t) => t.name === item.name) + if (realIndex !== index) { + // Update the "real" contributor to also include the noreply's commits + self[realIndex].commits += item.commits + return false + } + return true + } + return true + }) +} + +export const resolveContributors = ( + commits: MergedRawCommit[], + gitProvider: KnownGitProvider | null, + options: ContributorsOptions, + extraContributors: string[] = [], +): GitContributor[] => { + const contributors = getRawContributors(commits, options, gitProvider) + + if (options.info?.length && extraContributors.length) { + for (const extraContributor of extraContributors) { + if (contributors.every((item) => item.name !== extraContributor)) { + const contributorInfo = getContributorInfo( + extraContributor, + options.info, + ) + + if (!contributorInfo) continue + + const result: GitContributor = { + name: contributorInfo.name ?? extraContributor, + email: '', + commits: 0, + } + + const url = + contributorInfo.url ?? + (gitProvider === 'github' + ? `https://github.com/${contributorInfo.username}` + : null) + + if (options.avatar) + result.avatar = + contributorInfo.avatar ?? + (gitProvider === 'github' + ? `https://avatars.githubusercontent.com/${contributorInfo.username}?v=4` + : `https://gravatar.com/avatar/${digestSHA256(contributorInfo.username)}?d=retro`) + + if (url) result.url = url + + contributors.push(result) + } + } + } + + return options.transform?.(contributors) ?? contributors +} diff --git a/plugins/development/plugin-git/src/node/typings.ts b/plugins/development/plugin-git/src/node/typings.ts new file mode 100644 index 000000000..166365f9b --- /dev/null +++ b/plugins/development/plugin-git/src/node/typings.ts @@ -0,0 +1,119 @@ +/** + * Git provider + */ +export type KnownGitProvider = 'bitbucket' | 'gitee' | 'github' | 'gitlab' + +/** + * Co-author information + */ +export interface CoAuthorInfo { + name: string + email: string +} + +export interface RawCommit { + /** + * File path + */ + filepath: string + /** + * Commit hash + */ + hash: string + /** + * Unix timestamp in milliseconds + */ + date: number + /** + * Commit message + */ + message: string + /** + * Commit message body + */ + body: string + /** + * Commit refs + */ + refs: string + /** + * Commit author name + */ + author: string + /** + * Commit author email + */ + email: string + + /** + * The co-authors of the commit + */ + coAuthors: CoAuthorInfo[] +} + +export interface MergedRawCommit extends Omit { + filepaths: string[] +} + +export interface GitContributor { + /** + * Contributor name + */ + name: string + /** + * Contributor email + */ + email: string + /** + * Number of commits + */ + commits: number + /** + * Contributor avatar + */ + avatar?: string + /** + * The url of the contributor + */ + url?: string +} + +export interface GitChangelog { + /** + * Commit hash + */ + hash: string + /** + * Unix timestamp in milliseconds + */ + date: number + /** + * Commit message + */ + message: string + /** + * The url of the commit + */ + commitUrl?: string + /** + * release tag + */ + tag?: string + /** + * The url of the release tag + */ + tagUrl?: string + /** + * Commit author name + */ + author: string + /** + * Commit author email + */ + email: string + + /** + * The co-authors of the commit + */ + coAuthors?: CoAuthorInfo[] +} diff --git a/plugins/development/plugin-git/src/node/utils/checkGitRepo.ts b/plugins/development/plugin-git/src/node/utils/checkGitRepo.ts index 091f51034..62d1567ba 100644 --- a/plugins/development/plugin-git/src/node/utils/checkGitRepo.ts +++ b/plugins/development/plugin-git/src/node/utils/checkGitRepo.ts @@ -1,55 +1,13 @@ import { execaCommandSync } from 'execa' -import type { KnownGitProvider } from '../types.js' /** * Check if the git repo is valid */ export const checkGitRepo = (cwd: string): boolean => { try { - execaCommandSync('git log', { cwd }) + execaCommandSync('git status', { cwd }) return true } catch { return false } } - -const getRemoteUrl = (cwd: string): string => { - try { - const { stdout } = execaCommandSync('git remote get-url origin', { cwd }) - return stdout - } catch { - try { - const { stdout } = execaCommandSync('git remote', { cwd }) - const remote = stdout.split('\n')[0]?.trim() - if (remote) { - const { stdout: remoteUrl } = execaCommandSync( - `git remote get-url ${remote}`, - { - cwd, - }, - ) - return remoteUrl - } - return '' - } catch { - return '' - } - } -} - -export const inferGitProvider = (cwd: string): KnownGitProvider | null => { - const remoteUrl = getRemoteUrl(cwd) - if (remoteUrl.includes('github.com')) { - return 'github' - } - if (remoteUrl.includes('gitlab.com')) { - return 'gitlab' - } - if (remoteUrl.includes('gitee.com')) { - return 'gitee' - } - if (remoteUrl.includes('bitbucket.org')) { - return 'bitbucket' - } - return null -} diff --git a/plugins/development/plugin-git/src/node/utils/digestSHA256.ts b/plugins/development/plugin-git/src/node/utils/digestSHA256.ts new file mode 100644 index 000000000..662aec642 --- /dev/null +++ b/plugins/development/plugin-git/src/node/utils/digestSHA256.ts @@ -0,0 +1,9 @@ +import { createHash } from 'node:crypto' + +const hash = createHash('sha256') + +export const digestSHA256 = (message: string): string => { + hash.update(message) + + return hash.digest('hex') +} diff --git a/plugins/development/plugin-git/src/node/utils/getCommits.ts b/plugins/development/plugin-git/src/node/utils/getCommits.ts index 213a5a8cc..e0452c20a 100644 --- a/plugins/development/plugin-git/src/node/utils/getCommits.ts +++ b/plugins/development/plugin-git/src/node/utils/getCommits.ts @@ -1,5 +1,5 @@ import { execa } from 'execa' -import type { GitContributor, MergedRawCommit, RawCommit } from '../types.js' +import type { GitContributor, MergedRawCommit, RawCommit } from '../typings.js' const FORMAT = '%H|%an|%ae|%ad|%s|%d|%b' const SPLIT_CHAR = '[GIT_LOG_COMMIT_END]' @@ -30,7 +30,7 @@ const getCoAuthors = ( export const getRawCommits = async ( filepath: string, cwd: string, -): Promise => { +): Promise => { try { const { stdout } = await execa( 'git', @@ -45,35 +45,32 @@ export const getRawCommits = async ( ], { cwd }, ) - return stdout.replace(RE_SPLIT, '').split(`${SPLIT_CHAR}\n`) + + return stdout + .replace(RE_SPLIT, '') + .split(`${SPLIT_CHAR}\n`) + .filter(Boolean) + .map((rawString) => { + const [hash, author, email, date, message, refs, body] = rawString + .split('|') + .map((v) => v.trim()) + return { + filepath, + hash, + date: Number.parseInt(date, 10) * 1000, + message, + body, + refs, + author, + email, + coAuthors: getCoAuthors(body), + } + }) } catch { return [] } } -export const parseRawCommits = ( - rawCommits: string[], - filepath: string, -): RawCommit[] => - rawCommits - .filter((commit) => !!commit) - .map((raw) => { - const [hash, author, email, date, message, refs, body] = raw - .split('|') - .map((v) => v.trim()) - return { - filepath, - hash, - date: Number.parseInt(date, 10) * 1000, - message, - body, - refs, - author, - email, - coAuthors: getCoAuthors(body), - } - }) - export const mergeRawCommits = (commits: RawCommit[]): MergedRawCommit[] => { const commitMap = new Map() @@ -91,13 +88,11 @@ export const getCommits = async ( filepaths: string[], cwd: string, ): Promise => { - const commits = await Promise.all( - filepaths.map(async (filepath) => { - const rawCommits = await getRawCommits(filepath, cwd) - return parseRawCommits(rawCommits, filepath) - }), + const rawCommits = await Promise.all( + filepaths.map((filepath) => getRawCommits(filepath, cwd)), ) - return mergeRawCommits(commits.flat()).sort((a, b) => + + return mergeRawCommits(rawCommits.flat()).sort((a, b) => b.date - a.date > 0 ? 1 : -1, ) } diff --git a/plugins/development/plugin-git/src/node/utils/getContributorInfo.ts b/plugins/development/plugin-git/src/node/utils/getContributorInfo.ts new file mode 100644 index 000000000..3499609b6 --- /dev/null +++ b/plugins/development/plugin-git/src/node/utils/getContributorInfo.ts @@ -0,0 +1,13 @@ +import type { ContributorInfo } from '../options.js' + +const toArray = (value?: T | T[]): T[] => + Array.isArray(value) ? value : value ? [value] : [] + +export const getContributorInfo = ( + contributorName: string, + infos: ContributorInfo[] = [], +): ContributorInfo | null => + infos.find( + ({ username, alias }) => + username === contributorName || toArray(alias).includes(contributorName), + ) ?? null diff --git a/plugins/development/plugin-git/src/node/utils/getUserNameWithNoreplyEmail.ts b/plugins/development/plugin-git/src/node/utils/getUserNameWithNoreplyEmail.ts new file mode 100644 index 000000000..7017c4f10 --- /dev/null +++ b/plugins/development/plugin-git/src/node/utils/getUserNameWithNoreplyEmail.ts @@ -0,0 +1,4 @@ +export const getUserNameWithNoreplyEmail = (email: string): string | null => + email.endsWith('@users.noreply.github.com') + ? email.replace('@users.noreply.github.com', '').split('+')[1] + : null diff --git a/plugins/development/plugin-git/src/node/utils/index.ts b/plugins/development/plugin-git/src/node/utils/index.ts index be152ce04..24dd66a65 100644 --- a/plugins/development/plugin-git/src/node/utils/index.ts +++ b/plugins/development/plugin-git/src/node/utils/index.ts @@ -1,4 +1,6 @@ export * from './checkGitRepo.js' +export * from './digestSHA256.js' export * from './getCommits.js' -export * from './resolveContributors.js' -export * from './resolveChangelog.js' +export * from './getContributorInfo.js' +export * from './getUserNameWithNoreplyEmail.js' +export * from './inferGitProvider.js' diff --git a/plugins/development/plugin-git/src/node/utils/inferGitProvider.ts b/plugins/development/plugin-git/src/node/utils/inferGitProvider.ts new file mode 100644 index 000000000..b279e72ee --- /dev/null +++ b/plugins/development/plugin-git/src/node/utils/inferGitProvider.ts @@ -0,0 +1,48 @@ +import { execaCommandSync } from 'execa' +import type { KnownGitProvider } from '../typings.js' + +export const getRemoteUrl = (cwd: string): string => { + try { + const { stdout } = execaCommandSync('git remote get-url origin', { cwd }) + return stdout + } catch { + try { + const { stdout } = execaCommandSync('git remote', { cwd }) + const remote = stdout.split('\n')[0]?.trim() + if (remote) { + const { stdout: remoteUrl } = execaCommandSync( + `git remote get-url ${remote}`, + { + cwd, + }, + ) + return remoteUrl + } + return '' + } catch { + return '' + } + } +} + +export const inferGitProvider = (cwd: string): KnownGitProvider | null => { + const remoteUrl = getRemoteUrl(cwd) + + if (remoteUrl.includes('github.com')) { + return 'github' + } + + if (remoteUrl.includes('gitlab.com')) { + return 'gitlab' + } + + if (remoteUrl.includes('gitee.com')) { + return 'gitee' + } + + if (remoteUrl.includes('bitbucket.org')) { + return 'bitbucket' + } + + return null +} diff --git a/plugins/development/plugin-git/src/node/utils/resolveContributors.ts b/plugins/development/plugin-git/src/node/utils/resolveContributors.ts deleted file mode 100644 index 35f051f65..000000000 --- a/plugins/development/plugin-git/src/node/utils/resolveContributors.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { webcrypto } from 'node:crypto' -import type { - ContributorConfig, - ContributorsOptions, - GitContributor, - KnownGitProvider, - MergedRawCommit, -} from '../types.js' - -export const getUserNameWithNoreplyEmail = ( - email: string, -): string | undefined => { - if (email.endsWith('@users.noreply.github.com')) { - return email.replace('@users.noreply.github.com', '').split('+')[1] - } - return undefined -} - -const toArray = (value?: T | T[]): T[] => { - if (!value) return [] - return Array.isArray(value) ? value : [value] -} - -export const digestSHA256 = async (message: string): Promise => { - const encoded = new TextEncoder().encode(message) - const buffer = await webcrypto.subtle.digest('SHA-256', encoded) - return Array.from(new Uint8Array(buffer)) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') -} - -export const getContributorWithConfig = ( - list: ContributorConfig[], - name: string, -): ContributorConfig | undefined => { - return list.find( - (item) => item.username === name || toArray(item.alias).includes(name), - ) -} - -export const getRawContributors = async ( - commits: MergedRawCommit[], - options: ContributorsOptions, - gitProvider: KnownGitProvider | null = null, -): Promise => { - const contributors = new Map() - - for (const commit of commits) { - const authors = [ - { name: commit.author, email: commit.email }, - ...toArray(commit.coAuthors), - ] - for (const { name: author, email } of authors) { - const config = getContributorWithConfig( - options.list ?? [], - getUserNameWithNoreplyEmail(email) ?? author, - ) - const username = config?.username ?? author - const name = config?.name ?? username - - const contributor = contributors.get(name + email) - if (contributor) { - contributor.commits++ - } else { - const item: GitContributor = { - name, - email, - commits: 1, - } - - if (options.avatar) - item.avatar = - config?.avatar ?? - (gitProvider === 'github' - ? `https://avatars.githubusercontent.com/${username}?v=4` - : `https://gravatar.com/avatar/${await digestSHA256(email || username)}?d=retro`) - - const url = - (config?.url ?? gitProvider === 'github') - ? `https://github.com/${username}` - : undefined - if (url) item.url = url - - contributors.set(name + email, item) - } - } - } - - return Array.from(contributors.values()).filter((item, index, self) => { - // If one of the contributors is a "noreply" email address, and there's - // already a contributor with the same name, it is very likely a duplicate, - // so it can be removed. - if (item.email.split('@')[1]?.match(/no-?reply/)) { - const realIndex = self.findIndex((t) => t.name === item.name) - if (realIndex !== index) { - // Update the "real" contributor to also include the noreply's commits - self[realIndex].commits += item.commits - return false - } - return true - } - return true - }) -} - -export const resolveContributors = async ( - commits: MergedRawCommit[], - options: ContributorsOptions = {}, - gitProvider: KnownGitProvider | null = null, - extraContributors?: string[], -): Promise => { - let contributors = await getRawContributors(commits, options, gitProvider) - - if (options.list?.length && extraContributors?.length) { - for (const extraContributor of extraContributors) { - if (!contributors.some((item) => item.name === extraContributor)) { - const config = getContributorWithConfig(options.list, extraContributor) - if (!config) continue - - const item: GitContributor = { - name: config.name ?? extraContributor, - email: '', - commits: 0, - } - - if (options.avatar) - item.avatar = - config.avatar ?? - (gitProvider === 'github' - ? `https://avatars.githubusercontent.com/${config.username}?v=4` - : `https://gravatar.com/avatar/${await digestSHA256(config.username)}?d=retro`) - - const url = - (config.url ?? gitProvider === 'github') - ? `https://github.com/${config.username}` - : undefined - if (url) item.url = url - - contributors.push(item) - } - } - } - - if (options.transform) contributors = await options.transform(contributors) - - return contributors -}