Refactor APIs, add getFileContent, update config and main.
This commit is contained in:
3
REAME.md
Normal file
3
REAME.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
|
github fine-grained permissions required: `metadata`, `content`
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>> {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
95
src/index.ts
95
src/index.ts
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user