diff --git a/REAME.md b/REAME.md new file mode 100644 index 0000000..f1da48d --- /dev/null +++ b/REAME.md @@ -0,0 +1,3 @@ + + +github fine-grained permissions required: `metadata`, `content` diff --git a/src/apis/gitea.ts b/src/apis/gitea.ts index 4d008d4..5a6221d 100644 --- a/src/apis/gitea.ts +++ b/src/apis/gitea.ts @@ -7,20 +7,26 @@ import * as result from "../result.js" // /api/swagger const Prefix = "/api/v1" -const IssueTemplatesUrl = (user: string, repo: string) => - `${Prefix}/repos/${user}/${repo}/issue_templates` +const urls = { + IssueTemplatesUrl: (user: string, repo: string) => + `${Prefix}/repos/${user}/${repo}/issue_templates`, + FileContentUrl: (user: string, repo: string, filepath: string) => + `${Prefix}/repos/${user}/${repo}/contents/${filepath}` +} -type GiteaIssueTemplatesResponse = { - name: string, - title: string, - about: string, - labels: string[] | null, - assignees: string[] | null, - ref: string, - content: string, - body: unknown | null, - file_name: string -}[] +namespace GiteaApi { + export type IssueTemplatesResponse = { + 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 @@ -33,7 +39,9 @@ class Gitea extends BaseApi { } async getIssueTemplates(): Promise> { - const url = new URL(IssueTemplatesUrl(this.user, this.repo), this.host) + const url = new URL( + urls.IssueTemplatesUrl(this.user, this.repo), + this.host) const response = await fetch(url, { headers: this.headers }) switch (response.status) { @@ -42,11 +50,19 @@ class Gitea extends BaseApi { default: return result.error("Unknown status code") } - const json: GiteaIssueTemplatesResponse = await response.json() + const json: GiteaApi.IssueTemplatesResponse = await response.json() return result.value( json.map(template => template.file_name) ) } + + async getFileContent(filepath: string): Promise> { + const url = new URL( + urls.FileContentUrl(this.user, this.repo, filepath), + this.host) + + + } } diff --git a/src/apis/github.ts b/src/apis/github.ts index 5886045..050e6ba 100644 --- a/src/apis/github.ts +++ b/src/apis/github.ts @@ -1,20 +1,9 @@ -import { URL } from "url" - import { BaseApi } from "./index.js" import type { Result } from "../result.js" import * as result from "../result.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" -*/ +// docs.github.com/en/rest const IssueTemplateFolders = [ "ISSUE_TEMPLATE", "issue_template", @@ -24,36 +13,38 @@ const IssueTemplateFolders = [ ".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 - // ... +const urls = { + // https://docs.github.com/en/rest/repos/repos#get-a-repository + RepoInfoUrl: (user: string, repo: string) => + `${Prefix}/repos/${user}/${repo}`, + + // https://docs.github.com/en/rest/git/trees#get-a-tree + TreeUrl: (user: string, repo: string, branch: string) => + `${Prefix}/repos/${user}/${repo}/git/trees/${branch}?recursive=1` + } -// 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` +namespace GitHubApi { + export interface RepoInfoResponse { + default_branch: string + // ... + } -interface GitHubTreeResponse { - sha: string - url: string - tree: { - path: string - mode: string - type: "tree" | "blob" + export interface TreeResponse { sha: string - size: number 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 @@ -65,8 +56,8 @@ class GitHub extends BaseApi { } } - async getDefaultBranch(): Promise> { - const url = new URL(RepoInfoUrl(this.user, this.repo), this.host) + private async getDefaultBranch(): Promise> { + const url = urls.RepoInfoUrl(this.user, this.repo) const response = await fetch(url, { headers: this.headers }) switch (response.status) { @@ -77,16 +68,21 @@ class GitHub extends BaseApi { default: return result.error("Unknown status code") } - const json: GitHubRepoInfoResponse = await response.json() + const json: GitHubApi.RepoInfoResponse = await response.json() return result.value(json.default_branch) } - async getRepoFiles(): Promise> { + private async getRepoFiles(): Promise> { const branch = await this.getDefaultBranch() if (!branch.success) return result.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 }) + const url = urls.TreeUrl(this.user, this.repo, branch.value) + let response: Response + try { + response = await fetch(url, { headers: this.headers }) + } catch (err) { + return result.error(`Fetch Error: ${err}`) + } switch (response.status) { case 200: break @@ -96,7 +92,13 @@ class GitHub extends BaseApi { default: return result.error("Unknown status code") } - const json: GitHubTreeResponse = await response.json() + let json: GitHubApi.TreeResponse + try { + json = await response.json() + } catch (error) { + return result.error(`Json Error: ${error}`) + } + return result.value(json.tree .filter(entry => entry.type == "blob") .map(entry => entry.path) @@ -112,6 +114,10 @@ class GitHub extends BaseApi { return result.value(issue_templates) } + + async getFileContent(filepath: string): Promise> { + + } } diff --git a/src/apis/index.ts b/src/apis/index.ts index 2f12030..a3795b7 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -13,4 +13,5 @@ export abstract class BaseApi { * @returns Array of filepath's */ abstract getIssueTemplates(): Promise> + abstract getFileContent(filepath: string): Promise> } diff --git a/src/config.ts b/src/config.ts index 8d6f518..36ea83f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,7 @@ import type { Result } from "./result.js" import * as result from "./result.js" -interface Repo { +export interface Repository { url: URL token: string client?: "gitea" | "github" @@ -48,7 +48,7 @@ function validateClient(client: unknown): Result { return result.value(undefined) } -function validateConfig(config: Record): Result { +export function validateConfig(config: Record): Result { if (!("repo" in config) || !Array.isArray(config["repo"]) ) return result.error("Missing repo array of tables") @@ -72,7 +72,7 @@ function validateConfig(config: Record): Result { return result.value(config["repo"]) } -export async function getConfig(config_filepath: string): Promise> { +export async function getConfig(config_filepath: string): Promise> { let configFile: string try { configFile = await fs.readFile(config_filepath, "utf8") diff --git a/src/index.ts b/src/index.ts index db2f06b..5499d78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,27 +1,90 @@ import * as cli from "./cli.js" -import { getConfig } from "./config.js" +import { getConfig, type Repository } from "./config.js" - -const cliConfig = cli.parseArgs(process.argv) -// console.log(cliConfig) +import type { Result } from "./result.js" +import * as result from "./result.js" 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", "s") -// -// const result = await api.getIssueTemplates() -// console.log(result.value ?? result.error) +function identifyGitClient(repo: Repository): "gitea" | "github" { + if (repo.client) + return repo.client + + if (repo.url.host === "github.com") + return "github" + + return "gitea" +} + +function instantiateApi(repo: Repository): Result, string> { + const client = identifyGitClient(repo) + const host = repo.url.origin + const token = repo.token + const path_parts = repo.url.pathname + .split("/") + .filter(part => part && part != "/") + if (path_parts.length != 2) + return result.error("Invalid repo url") + const [user, repo_name] = path_parts as [string, string] + + const ApiClass = client === "gitea" ? Gitea : GitHub + return result.value( + new ApiClass(host, token, user, repo_name) + ) +} + +async function handleTemplate(template_file: string): Promise> { -const config = await getConfig(cliConfig.config_filepath) -console.log(config) + + return result.value(undefined) +} + +async function handleRepo(repo: Repository): Promise> { + const api = instantiateApi(repo) + if (!api.success) + return result.error(`instantiateApi: ${api.error}`) + + const templates = await api.value.getIssueTemplates() + if (!templates.success) + return result.error(`getIssueTemplates: ${templates.error}`) + + for (const template of templates.value) { + const result = await handleTemplate(template) + console.log(result) + } + + return result.value(undefined) +} + +async function main() { + const cliConfig = cli.parseArgs(process.argv) + + const config = await getConfig(cliConfig.config_filepath) + if (!config.success) { + console.error(config.error) + process.exit(1) + } + + const promise_results = await Promise.allSettled( + config.value.map(repo => handleRepo(repo)) + ) + for (const p_result of promise_results) { + if (p_result.status === "rejected") { + console.error(p_result.reason) + continue + } + + if (!p_result.value.success) { + console.error(p_result.value.error) + continue + } + + console.log(p_result.value.value) + } +} + +await main()