Refactor APIs, add getFileContent, update config and main.

This commit is contained in:
2025-10-27 23:16:47 +00:00
parent 0989f1ca30
commit 8b62f72f93
6 changed files with 166 additions and 77 deletions

3
REAME.md Normal file
View File

@@ -0,0 +1,3 @@
github fine-grained permissions required: `metadata`, `content`

View File

@@ -7,10 +7,15 @@ import * as result from "../result.js"
// /api/swagger // /api/swagger
const Prefix = "/api/v1" const Prefix = "/api/v1"
const IssueTemplatesUrl = (user: string, repo: string) => const urls = {
`${Prefix}/repos/${user}/${repo}/issue_templates` 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 = { namespace GiteaApi {
export type IssueTemplatesResponse = {
name: string, name: string,
title: string, title: string,
about: string, about: string,
@@ -21,6 +26,7 @@ type GiteaIssueTemplatesResponse = {
body: unknown | null, body: unknown | null,
file_name: string file_name: string
}[] }[]
}
class Gitea extends BaseApi { class Gitea extends BaseApi {
headers: Record<string, string> headers: Record<string, string>
@@ -33,7 +39,9 @@ class Gitea extends BaseApi {
} }
async getIssueTemplates(): Promise<Result<string[], string>> { async getIssueTemplates(): Promise<Result<string[], string>> {
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 }) const response = await fetch(url, { headers: this.headers })
switch (response.status) { switch (response.status) {
@@ -42,11 +50,19 @@ class Gitea extends BaseApi {
default: return result.error("Unknown status code") default: return result.error("Unknown status code")
} }
const json: GiteaIssueTemplatesResponse = await response.json() const json: GiteaApi.IssueTemplatesResponse = await response.json()
return result.value( return result.value(
json.map(template => template.file_name) json.map(template => template.file_name)
) )
} }
async getFileContent(filepath: string): Promise<Result<string[], string>> {
const url = new URL(
urls.FileContentUrl(this.user, this.repo, filepath),
this.host)
}
} }

View File

@@ -1,20 +1,9 @@
import { URL } from "url"
import { BaseApi } from "./index.js" import { BaseApi } from "./index.js"
import type { Result } from "../result.js" import type { Result } from "../result.js"
import * as 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 = [ const IssueTemplateFolders = [
"ISSUE_TEMPLATE", "ISSUE_TEMPLATE",
"issue_template", "issue_template",
@@ -24,23 +13,25 @@ const IssueTemplateFolders = [
".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" const Prefix = "https://api.github.com"
const urls = {
// https://docs.github.com/en/rest/repos/repos#get-a-repository // https://docs.github.com/en/rest/repos/repos#get-a-repository
const RepoInfoUrl = (user: string, repo: string) => RepoInfoUrl: (user: string, repo: string) =>
`${Prefix}/repos/${user}/${repo}` `${Prefix}/repos/${user}/${repo}`,
interface GitHubRepoInfoResponse {
// 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`
}
namespace GitHubApi {
export interface RepoInfoResponse {
default_branch: string default_branch: string
// ... // ...
} }
// https://docs.github.com/en/rest/git/trees#get-a-tree export interface TreeResponse {
const TreeUrl = (user: string, repo: string, branch: string) =>
`${Prefix}/repos/${user}/${repo}/git/trees/${branch}?recursive=1`
interface GitHubTreeResponse {
sha: string sha: string
url: string url: string
tree: { tree: {
@@ -53,7 +44,7 @@ interface GitHubTreeResponse {
}[] }[]
} }
// fine-grained tokens required permissions: metadata, content }
class GitHub extends BaseApi { class GitHub extends BaseApi {
headers: Record<string, string> headers: Record<string, string>
@@ -65,8 +56,8 @@ class GitHub extends BaseApi {
} }
} }
async getDefaultBranch(): Promise<Result<string, string>> { private async getDefaultBranch(): Promise<Result<string, string>> {
const url = new URL(RepoInfoUrl(this.user, this.repo), this.host) const url = urls.RepoInfoUrl(this.user, this.repo)
const response = await fetch(url, { headers: this.headers }) const response = await fetch(url, { headers: this.headers })
switch (response.status) { switch (response.status) {
@@ -77,16 +68,21 @@ class GitHub extends BaseApi {
default: return result.error("Unknown status code") 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) return result.value(json.default_branch)
} }
async getRepoFiles(): Promise<Result<string[], string>> { private async getRepoFiles(): Promise<Result<string[], string>> {
const branch = await this.getDefaultBranch() const branch = await this.getDefaultBranch()
if (!branch.success) return result.error(`Default branch error: ${branch.error}`) 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 url = urls.TreeUrl(this.user, this.repo, branch.value)
const response = await fetch(url, { headers: this.headers }) let response: Response
try {
response = await fetch(url, { headers: this.headers })
} catch (err) {
return result.error(`Fetch Error: ${err}`)
}
switch (response.status) { switch (response.status) {
case 200: break case 200: break
@@ -96,7 +92,13 @@ class GitHub extends BaseApi {
default: return result.error("Unknown status code") 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 return result.value(json.tree
.filter(entry => entry.type == "blob") .filter(entry => entry.type == "blob")
.map(entry => entry.path) .map(entry => entry.path)
@@ -112,6 +114,10 @@ class GitHub extends BaseApi {
return result.value(issue_templates) return result.value(issue_templates)
} }
async getFileContent(filepath: string): Promise<Result<string[], string>> {
}
} }

View File

@@ -13,4 +13,5 @@ export abstract class BaseApi {
* @returns Array of filepath's * @returns Array of filepath's
*/ */
abstract getIssueTemplates(): Promise<Result<string[], string>> abstract getIssueTemplates(): Promise<Result<string[], string>>
abstract getFileContent(filepath: string): Promise<Result<string, string>>
} }

View File

@@ -4,7 +4,7 @@ import type { Result } from "./result.js"
import * as result from "./result.js" import * as result from "./result.js"
interface Repo { export interface Repository {
url: URL url: URL
token: string token: string
client?: "gitea" | "github" client?: "gitea" | "github"
@@ -48,7 +48,7 @@ function validateClient(client: unknown): Result<undefined, string> {
return result.value(undefined) return result.value(undefined)
} }
function validateConfig(config: Record<string, any>): Result<Repo[], string> { export function validateConfig(config: Record<string, any>): Result<Repository[], string> {
if (!("repo" in config) || if (!("repo" in config) ||
!Array.isArray(config["repo"]) !Array.isArray(config["repo"])
) return result.error("Missing repo array of tables") ) return result.error("Missing repo array of tables")
@@ -72,7 +72,7 @@ function validateConfig(config: Record<string, any>): Result<Repo[], string> {
return result.value(config["repo"]) return result.value(config["repo"])
} }
export async function getConfig(config_filepath: string): Promise<Result<Repo[], string>> { export async function getConfig(config_filepath: string): Promise<Result<Repository[], string>> {
let configFile: string let configFile: string
try { try {
configFile = await fs.readFile(config_filepath, "utf8") configFile = await fs.readFile(config_filepath, "utf8")

View File

@@ -1,27 +1,90 @@
import * as cli from "./cli.js" import * as cli from "./cli.js"
import { getConfig } from "./config.js" import { getConfig, type Repository } from "./config.js"
import type { Result } from "./result.js"
const cliConfig = cli.parseArgs(process.argv) import * as result from "./result.js"
// console.log(cliConfig)
import type { BaseApi } from "./apis"; import type { BaseApi } from "./apis";
import Gitea from "./apis/gitea.js" import Gitea from "./apis/gitea.js"
import GitHub from "./apis/github.js"; import GitHub from "./apis/github.js";
// let api: BaseApi function identifyGitClient(repo: Repository): "gitea" | "github" {
// api = new Gitea( if (repo.client)
// "http://localhost:3000", "...", return repo.client
// "admin", "test_repo"
// )
// api = new GitHub(
// "https://api.github.com", "...",
// "TxFig", "s")
//
// const result = await api.getIssueTemplates()
// console.log(result.value ?? result.error)
if (repo.url.host === "github.com")
return "github"
return "gitea"
}
function instantiateApi(repo: Repository): Result<InstanceType<typeof BaseApi>, 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<Result<undefined, string>> {
return result.value(undefined)
}
async function handleRepo(repo: Repository): Promise<Result<undefined, string>> {
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) const config = await getConfig(cliConfig.config_filepath)
console.log(config) 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()