diff --git a/src/apis/gitea.ts b/src/apis/gitea.ts new file mode 100644 index 0000000..8377091 --- /dev/null +++ b/src/apis/gitea.ts @@ -0,0 +1,51 @@ +import { URL } from "url" + +import { BaseApi } from "./index.js" + + +// /api/swagger +const Prefix = "/api/v1" +const IssueTemplatesUrl = (user: string, repo: string) => + `${Prefix}/repos/${user}/${repo}/issue_templates` + +type GiteaIssueTemplatesResponse = { + name: string, + title: string, + about: string, + labels: string[] | null, + assignees: string[] | null, + ref: string, + content: string, + body: unknown | null, + file_name: string +}[] + +class Gitea extends BaseApi { + headers: Record + + constructor(...options: ConstructorParameters) { + super(...options) + this.headers = { + "Authorization": `token ${this.token}` + } + } + + async getIssueTemplates() { + const url = new URL(IssueTemplatesUrl(this.user, this.repo), this.host) + const response = await fetch(url, { headers: this.headers }) + + switch (response.status) { + case 200: break + case 404: return { error: "Issue templates not found" } + default: return { error: "Unknown status code" } + } + + const json: GiteaIssueTemplatesResponse = await response.json() + return { + value: json.map(template => template.file_name) + } + } +} + + +export default Gitea diff --git a/src/apis/github.ts b/src/apis/github.ts new file mode 100644 index 0000000..9a5776b --- /dev/null +++ b/src/apis/github.ts @@ -0,0 +1,118 @@ +import { URL } from "url" + +import {BaseApi, type Result} from "./index.js" + + +// docs.github.com/en/rest +/* +Folders searched by gitea: + "ISSUE_TEMPLATE" + "issue_template" + ".gitea/ISSUE_TEMPLATE" + ".gitea/issue_template" + ".github/ISSUE_TEMPLATE" + ".github/issue_template" +*/ + +const IssueTemplateFolders = [ + "ISSUE_TEMPLATE", + "issue_template", + ".gitea/ISSUE_TEMPLATE", + ".gitea/issue_template", + ".github/ISSUE_TEMPLATE", + ".github/issue_template" +] + +// /repos/${user}/${repo} -> .default_branch +// /repos/${user}/${repo}/git/trees/{branch}?recursive=1 -> search for templates + +const Prefix = "https://api.github.com" +// https://docs.github.com/en/rest/repos/repos#get-a-repository +const RepoInfoUrl = (user: string, repo: string) => + `${Prefix}/repos/${user}/${repo}` +interface GitHubRepoInfoResponse { + default_branch: string + // ... +} + +// https://docs.github.com/en/rest/git/trees#get-a-tree +const TreeUrl = (user: string, repo: string, branch: string) => + `${Prefix}/repos/${user}/${repo}/git/trees/${branch}?recursive=1` + +interface GitHubTreeResponse { + sha: string + url: string + tree: { + path: string + mode: string + type: "tree" | "blob" + sha: string + size: number + url: string + }[] +} + +// fine-grained tokens required permissions: metadata, content + +class GitHub extends BaseApi { + headers: Record + + constructor(...options: ConstructorParameters) { + super(...options) + this.headers = { + "Authorization": `token ${this.token}` + } + } + + async getDefaultBranch(): Promise> { + const url = new URL(RepoInfoUrl(this.user, this.repo), this.host) + const response = await fetch(url, { headers: this.headers }) + + switch (response.status) { + case 301: return { error: "Moved permanently" } + case 403: return { error: "Forbidden" } + case 404: return { error: "Resource not found" } + case 200: break + default: return { error: "Unknown status code" } + } + + const json: GitHubRepoInfoResponse = await response.json() + return { value: json.default_branch } + } + + async getRepoFiles(): Promise> { + const branch = await this.getDefaultBranch() + if (!branch.value) return { error: `Default branch error: ${branch.error}` } + + const url = new URL(TreeUrl(this.user, this.repo, branch.value), this.host) + const response = await fetch(url, { headers: this.headers }) + + switch (response.status) { + case 200: break + case 404: return { error: "Resource not found" } + case 409: return { error: "Conflict" } + case 422: return { error: "Validation failed, or the endpoint has been spammed" } + default: return { error: "Unknown status code" } + } + + const json: GitHubTreeResponse = await response.json() + return { + value: json.tree + .filter(entry => entry.type == "blob") + .map(entry => entry.path) + } + } + + async getIssueTemplates() { + const files = await this.getRepoFiles() + if (!files.value) return { error: `Repo files error: ${files.error}` } + const issue_templates = files.value.filter( + file => IssueTemplateFolders.some(folder => file.startsWith(folder)) + ) + + return { value: issue_templates } + } +} + + +export default GitHub diff --git a/src/apis/index.ts b/src/apis/index.ts new file mode 100644 index 0000000..b534953 --- /dev/null +++ b/src/apis/index.ts @@ -0,0 +1,21 @@ +export type Result = { + value: Type, + error?: null +} | { + value?: null, + error: Err +} + +export abstract class BaseApi { + constructor( + readonly host: string, + readonly token: string, + readonly user: string, + readonly repo: string + ) {} + + /** + * @returns Array of filepath's + */ + abstract getIssueTemplates(): Promise> +} diff --git a/src/index.ts b/src/index.ts index 5b7ac69..9299ab2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,21 @@ import * as cli from "./cli.js" const config = cli.parseArgs(process.argv) -console.log(config) +// console.log(config) + +import type { BaseApi } from "./apis"; +import Gitea from "./apis/gitea.js" +import GitHub from "./apis/github.js"; + + +let api: BaseApi +api = new Gitea( + "http://localhost:3000", "...", + "admin", "test_repo" +) +api = new GitHub( + "https://api.github.com", "...", + "TxFig", "test-api") + +const result = await api.getIssueTemplates() +console.log(result.value ?? result.error)