From 0bb28ab94d4a8f969d89728297b63c15451c5b5e Mon Sep 17 00:00:00 2001 From: Donald Shtjefni Date: Wed, 1 Mar 2023 22:35:43 +0100 Subject: [PATCH] feat!: support different repository providers (#55) * fix: broken ungh link * feat!: :sparkles: support for multiple git hosts addeedd support for github, gitlab, bitbucket and selfhosted git hosts removed github from config * test: multiple git hosts * chore: merge * refactor: rename host to repo * simplify repo logic * lint code * refactor regex to top --------- Co-authored-by: Pooya Parsa --- src/config.ts | 13 ++-- src/index.ts | 1 + src/markdown.ts | 31 ++------- src/repo.ts | 101 +++++++++++++++++++++++++++++ test/git.test.ts | 164 ++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 277 insertions(+), 33 deletions(-) create mode 100644 src/repo.ts diff --git a/src/config.ts b/src/config.ts index da5ced1..99e6dcd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,13 +2,15 @@ import { resolve } from "node:path"; import { loadConfig } from "c12"; import { readPackageJSON } from "pkg-types"; import { getLastGitTag, getCurrentGitRef } from "./git"; +import { getRepoConfig } from "./repo"; import type { SemverBumpType } from "./semver"; +import type { RepoConfig } from "./repo"; export interface ChangelogConfig { cwd: string; types: Record; scopeMap: Record; - github: string; + repo?: RepoConfig; from: string; to: string; newVersion?: string; @@ -31,7 +33,6 @@ const ConfigDefaults: ChangelogConfig = { ci: { title: "🤖 CI" }, }, cwd: null, - github: "", from: "", to: "", output: "CHANGELOG.md", @@ -69,16 +70,14 @@ export async function loadChangelogConfig( : resolve(cwd, config.output); } - if (!config.github) { + if (!config.repo) { const pkg = await readPackageJSON(cwd).catch(() => {}); if (pkg && pkg.repository) { - const repo = + const repoUrl = typeof pkg.repository === "string" ? pkg.repository : pkg.repository.url; - if (/^\w+\/\w+$/.test(repo)) { - config.github = repo; - } + config.repo = getRepoConfig(repoUrl); } } diff --git a/src/index.ts b/src/index.ts index 5881b27..f51fb07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from "./git"; export * from "./markdown"; export * from "./config"; export * from "./semver"; +export * from "./repo"; diff --git a/src/markdown.ts b/src/markdown.ts index a62960c..6a95598 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -3,6 +3,7 @@ import { convert } from "convert-gitmoji"; import { fetch } from "node-fetch-native"; import type { ChangelogConfig } from "./config"; import type { GitCommit, Reference } from "./git"; +import { formatReference, formatCompareChanges } from "./repo"; export async function generateMarkDown( commits: GitCommit[], @@ -17,13 +18,8 @@ export async function generateMarkDown( const v = config.newVersion && `v${config.newVersion}`; markdown.push("", "## " + (v || `${config.from}...${config.to}`), ""); - if (config.github) { - markdown.push( - `[compare changes](https://github.com/${config.github}/compare/${ - config.from - }...${v || config.to})`, - "" - ); + if (config.repo) { + markdown.push(formatCompareChanges(v, config), ""); } for (const type in config.types) { @@ -112,33 +108,20 @@ function formatCommit(commit: GitCommit, config: ChangelogConfig) { ); } -const refTypeMap: Record = { - "pull-request": "pull", - hash: "commit", - issue: "ssue", -}; - -function formatReference(ref: Reference, config: ChangelogConfig) { - if (!config.github) { - return ref.value; - } - return `[${ref.value}](https://github.com/${config.github}/${ - refTypeMap[ref.type] - }/${ref.value.replace(/^#/, "")})`; -} - function formatReferences(references: Reference[], config: ChangelogConfig) { const pr = references.filter((ref) => ref.type === "pull-request"); const issue = references.filter((ref) => ref.type === "issue"); if (pr.length > 0 || issue.length > 0) { return ( " (" + - [...pr, ...issue].map((ref) => formatReference(ref, config)).join(", ") + + [...pr, ...issue] + .map((ref) => formatReference(ref, config.repo)) + .join(", ") + ")" ); } if (references.length > 0) { - return " (" + formatReference(references[0], config) + ")"; + return " (" + formatReference(references[0], config.repo) + ")"; } return ""; } diff --git a/src/repo.ts b/src/repo.ts new file mode 100644 index 0000000..8258abf --- /dev/null +++ b/src/repo.ts @@ -0,0 +1,101 @@ +import type { Reference } from "./git"; +import type { ChangelogConfig } from "./config"; + +export type RepoProvider = "github" | "gitlab" | "bitbucket"; + +export type RepoConfig = { + domain?: string; + repo?: string; + provider?: RepoProvider; +}; + +const providerToRefSpec: Record< + RepoProvider, + Record +> = { + github: { "pull-request": "pull", hash: "commit", issue: "issues" }, + gitlab: { "pull-request": "merge_requests", hash: "commit", issue: "issues" }, + bitbucket: { + "pull-request": "pull-requests", + hash: "commit", + issue: "issues", + }, +}; + +const providerToDomain: Record = { + github: "github.com", + gitlab: "gitlab.com", + bitbucket: "bitbucket.org", +}; + +const domainToProvider: Record = { + "github.com": "github", + "gitlab.com": "gitlab", + "bitbucket.org": "bitbucket", +}; + +// https://regex101.com/r/NA4Io6/1 +const providerURLRegex = + /^(?:(?\w+)@)?(?:(?[^/:]+):)?(?\w+\/\w+)(?:\.git)?$/; + +function baseUrl(config: RepoConfig) { + return `https://${config.domain}/${config.repo}`; +} + +export function formatReference(ref: Reference, repo?: RepoConfig) { + if (!repo || !(repo.provider in providerToRefSpec)) { + return ref.value; + } + const refSpec = providerToRefSpec[repo.provider]; + return `[${ref.value}](${baseUrl(repo)}/${ + refSpec[ref.type] + }/${ref.value.replace(/^#/, "")})`; +} + +export function formatCompareChanges(v: string, config: ChangelogConfig) { + const part = + config.repo.provider === "bitbucket" ? "branches/compare" : "compare"; + return `[compare changes](${baseUrl(config.repo)}/${part}/${config.from}...${ + v || config.to + })`; +} + +export function getRepoConfig(repoUrl = ""): RepoConfig { + let provider; + let repo; + let domain; + + let url; + try { + url = new URL(repoUrl); + } catch {} + + const m = repoUrl.match(providerURLRegex)?.groups ?? {}; + if (m.repo && m.provider) { + repo = m.repo; + provider = + m.provider in domainToProvider + ? domainToProvider[m.provider] + : m.provider; + domain = + provider in providerToDomain ? providerToDomain[provider] : provider; + } else if (url) { + domain = url.hostname; + repo = url.pathname + .split("/") + .slice(1, 3) + .join("/") + .replace(/\.git$/, ""); + provider = domainToProvider[domain]; + } else if (m.repo) { + repo = m.repo; + provider = "github"; + domain = providerToDomain[provider]; + } + + return { + provider, + repo, + domain, + }; +} diff --git a/test/git.test.ts b/test/git.test.ts index a51f9ae..bf71594 100644 --- a/test/git.test.ts +++ b/test/git.test.ts @@ -4,7 +4,10 @@ import { getGitDiff, loadChangelogConfig, parseCommits, + getRepoConfig, + formatReference, } from "../src"; +import { RepoConfig } from "./../src/repo"; describe("git", () => { test("getGitDiff should work", async () => { @@ -295,7 +298,7 @@ describe("git", () => { ### 🩹 Fixes - Consider docs and refactor as semver patch for bump ([648ccf1](https://github.com/unjs/changelogen/commit/648ccf1)) - - **scope:** ⚠️ Breaking change example, close #123 ([#134](https://github.com/unjs/changelogen/pull/134), [#123](https://github.com/unjs/changelogen/ssue/123)) + - **scope:** ⚠️ Breaking change example, close #123 ([#134](https://github.com/unjs/changelogen/pull/134), [#123](https://github.com/unjs/changelogen/issues/123)) ### 🏡 Chore @@ -308,11 +311,168 @@ describe("git", () => { #### ⚠️ Breaking Changes - - **scope:** ⚠️ Breaking change example, close #123 ([#134](https://github.com/unjs/changelogen/pull/134), [#123](https://github.com/unjs/changelogen/ssue/123)) + - **scope:** ⚠️ Breaking change example, close #123 ([#134](https://github.com/unjs/changelogen/pull/134), [#123](https://github.com/unjs/changelogen/issues/123)) ### ❤️ Contributors - Pooya Parsa ([@pi0](http://github.com/pi0))" `); }); + + test("parse host config", () => { + expect(getRepoConfig(undefined)).toMatchObject({}); + expect(getRepoConfig("")).toMatchObject({}); + expect(getRepoConfig("unjs")).toMatchObject({}); + + const github = { + provider: "github", + repo: "unjs/changelogen", + domain: "github.com", + }; + expect(getRepoConfig("unjs/changelogen")).toStrictEqual(github); + expect(getRepoConfig("github:unjs/changelogen")).toStrictEqual(github); + expect(getRepoConfig("https://github.com/unjs/changelogen")).toStrictEqual( + github + ); + expect( + getRepoConfig("https://github.com/unjs/changelogen.git") + ).toStrictEqual(github); + expect(getRepoConfig("git@github.com:unjs/changelogen.git")).toStrictEqual( + github + ); + + const gitlab = { + provider: "gitlab", + repo: "unjs/changelogen", + domain: "gitlab.com", + }; + + expect(getRepoConfig("gitlab:unjs/changelogen")).toStrictEqual(gitlab); + expect(getRepoConfig("https://gitlab.com/unjs/changelogen")).toStrictEqual( + gitlab + ); + expect( + getRepoConfig("https://gitlab.com/unjs/changelogen.git") + ).toStrictEqual(gitlab); + expect(getRepoConfig("git@gitlab.com:unjs/changelogen.git")).toStrictEqual( + gitlab + ); + + const bitbucket = { + provider: "bitbucket", + repo: "unjs/changelogen", + domain: "bitbucket.org", + }; + + expect(getRepoConfig("bitbucket:unjs/changelogen")).toStrictEqual( + bitbucket + ); + expect( + getRepoConfig("https://bitbucket.org/unjs/changelogen") + ).toStrictEqual(bitbucket); + expect( + getRepoConfig("https://bitbucket.org/unjs/changelogen.git") + ).toStrictEqual(bitbucket); + expect( + getRepoConfig("https://donaldsh@bitbucket.org/unjs/changelogen.git") + ).toStrictEqual(bitbucket); + expect( + getRepoConfig("git@bitbucket.org:unjs/changelogen.git") + ).toStrictEqual(bitbucket); + + const selfhosted = { + repo: "unjs/changelogen", + domain: "git.unjs.io", + }; + + expect(getRepoConfig("selfhosted:unjs/changelogen")).toMatchObject({ + provider: "selfhosted", + repo: "unjs/changelogen", + }); + + expect(getRepoConfig("https://git.unjs.io/unjs/changelogen")).toMatchObject( + selfhosted + ); + + expect( + getRepoConfig("https://git.unjs.io/unjs/changelogen.git") + ).toMatchObject(selfhosted); + expect( + getRepoConfig("https://donaldsh@git.unjs.io/unjs/changelogen.git") + ).toMatchObject(selfhosted); + expect(getRepoConfig("git@git.unjs.io:unjs/changelogen.git")).toMatchObject( + selfhosted + ); + }); + + test("format reference", () => { + expect(formatReference({ type: "hash", value: "3828bda" })).toBe("3828bda"); + expect(formatReference({ type: "pull-request", value: "#123" })).toBe( + "#123" + ); + expect(formatReference({ type: "issue", value: "#14" })).toBe("#14"); + + const github: RepoConfig = { + provider: "github", + repo: "unjs/changelogen", + domain: "github.com", + }; + + expect(formatReference({ type: "hash", value: "3828bda" }, github)).toBe( + "[3828bda](https://github.com/unjs/changelogen/commit/3828bda)" + ); + expect( + formatReference({ type: "pull-request", value: "#123" }, github) + ).toBe("[#123](https://github.com/unjs/changelogen/pull/123)"); + expect(formatReference({ type: "issue", value: "#14" }, github)).toBe( + "[#14](https://github.com/unjs/changelogen/issues/14)" + ); + + const gitlab: RepoConfig = { + provider: "gitlab", + repo: "unjs/changelogen", + domain: "gitlab.com", + }; + + expect(formatReference({ type: "hash", value: "3828bda" }, gitlab)).toBe( + "[3828bda](https://gitlab.com/unjs/changelogen/commit/3828bda)" + ); + expect( + formatReference({ type: "pull-request", value: "#123" }, gitlab) + ).toBe("[#123](https://gitlab.com/unjs/changelogen/merge_requests/123)"); + expect(formatReference({ type: "issue", value: "#14" }, gitlab)).toBe( + "[#14](https://gitlab.com/unjs/changelogen/issues/14)" + ); + + const bitbucket: RepoConfig = { + provider: "bitbucket", + repo: "unjs/changelogen", + domain: "bitbucket.org", + }; + + expect(formatReference({ type: "hash", value: "3828bda" }, bitbucket)).toBe( + "[3828bda](https://bitbucket.org/unjs/changelogen/commit/3828bda)" + ); + expect( + formatReference({ type: "pull-request", value: "#123" }, bitbucket) + ).toBe("[#123](https://bitbucket.org/unjs/changelogen/pull-requests/123)"); + expect(formatReference({ type: "issue", value: "#14" }, bitbucket)).toBe( + "[#14](https://bitbucket.org/unjs/changelogen/issues/14)" + ); + + const unkown: RepoConfig = { + repo: "unjs/changelogen", + domain: "git.unjs.io", + }; + + expect(formatReference({ type: "hash", value: "3828bda" }, unkown)).toBe( + "3828bda" + ); + expect( + formatReference({ type: "pull-request", value: "#123" }, unkown) + ).toBe("#123"); + expect(formatReference({ type: "issue", value: "#14" }, unkown)).toBe( + "#14" + ); + }); });