refactor: js output
This commit is contained in:
parent
1aa28c2a34
commit
c6704c3a04
96 changed files with 3816 additions and 147 deletions
20
src/bugzilla/client.d.ts
vendored
Normal file
20
src/bugzilla/client.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { BugzillaConfig } from "./config.js";
|
||||
import type { BugzillaBug, BugzillaComment, BugzillaProduct, BugzillaComponent, BugzillaSearchParams, BugzillaCreatePayload, BugzillaUpdatePayload, BugzillaCreateProductPayload, BugzillaCreateComponentPayload, BugzillaUpdateResponse } from "./types.js";
|
||||
export declare class BugzillaClient {
|
||||
private baseUrl;
|
||||
private apiKey;
|
||||
constructor(config: BugzillaConfig);
|
||||
private request;
|
||||
getBug(id: number): Promise<BugzillaBug>;
|
||||
searchBugs(params: BugzillaSearchParams): Promise<BugzillaBug[]>;
|
||||
createBug(payload: BugzillaCreatePayload): Promise<number>;
|
||||
updateBug(id: number, payload: BugzillaUpdatePayload): Promise<BugzillaUpdateResponse>;
|
||||
getComments(bugId: number): Promise<BugzillaComment[]>;
|
||||
addComment(bugId: number, comment: string): Promise<number>;
|
||||
tagComment(commentId: number, tags: string[]): Promise<void>;
|
||||
getProduct(nameOrId: string | number): Promise<BugzillaProduct>;
|
||||
getAccessibleProducts(): Promise<BugzillaProduct[]>;
|
||||
createProduct(payload: BugzillaCreateProductPayload): Promise<number>;
|
||||
getComponents(product: string): Promise<BugzillaComponent[]>;
|
||||
createComponent(payload: BugzillaCreateComponentPayload): Promise<number>;
|
||||
}
|
||||
115
src/bugzilla/client.js
Normal file
115
src/bugzilla/client.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
export class BugzillaClient {
|
||||
baseUrl;
|
||||
apiKey;
|
||||
constructor(config) {
|
||||
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||
this.apiKey = config.apiKey;
|
||||
}
|
||||
async request(method, path, body) {
|
||||
// Auth: api_key in query for GET, in body for POST/PUT
|
||||
let url;
|
||||
if (method === "GET") {
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
url = `${this.baseUrl}${path}${sep}api_key=${encodeURIComponent(this.apiKey)}`;
|
||||
}
|
||||
else {
|
||||
url = `${this.baseUrl}${path}`;
|
||||
const bodyObj = (body ?? {});
|
||||
body = { ...bodyObj, api_key: this.apiKey };
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Bugzilla API ${method} ${path} failed (${res.status}): ${text}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
async getBug(id) {
|
||||
const data = await this.request("GET", `/bug/${id}`);
|
||||
if (!data.bugs?.length)
|
||||
throw new Error(`Bug ${id} not found`);
|
||||
return data.bugs[0];
|
||||
}
|
||||
async searchBugs(params) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.product)
|
||||
query.set("product", params.product);
|
||||
if (params.component)
|
||||
query.set("component", params.component);
|
||||
if (params.summary)
|
||||
query.set("summary", params.summary);
|
||||
if (params.limit)
|
||||
query.set("limit", String(params.limit));
|
||||
if (params.offset)
|
||||
query.set("offset", String(params.offset));
|
||||
if (params.last_change_time)
|
||||
query.set("last_change_time", params.last_change_time);
|
||||
if (params.status) {
|
||||
const statuses = Array.isArray(params.status) ? params.status : [params.status];
|
||||
for (const s of statuses)
|
||||
query.append("status", s);
|
||||
}
|
||||
if (params.id) {
|
||||
const ids = Array.isArray(params.id) ? params.id : [params.id];
|
||||
for (const id of ids)
|
||||
query.append("id", String(id));
|
||||
}
|
||||
if (params.alias) {
|
||||
const aliases = Array.isArray(params.alias) ? params.alias : [params.alias];
|
||||
for (const a of aliases)
|
||||
query.append("alias", a);
|
||||
}
|
||||
if (params.url)
|
||||
query.set("url", params.url);
|
||||
const qs = query.toString();
|
||||
const path = `/bug${qs ? `?${qs}` : ""}`;
|
||||
const data = await this.request("GET", path);
|
||||
return data.bugs ?? [];
|
||||
}
|
||||
async createBug(payload) {
|
||||
const data = await this.request("POST", "/bug", payload);
|
||||
return data.id;
|
||||
}
|
||||
async updateBug(id, payload) {
|
||||
return this.request("PUT", `/bug/${id}`, payload);
|
||||
}
|
||||
async getComments(bugId) {
|
||||
const data = await this.request("GET", `/bug/${bugId}/comment`);
|
||||
return data.bugs?.[String(bugId)]?.comments ?? [];
|
||||
}
|
||||
async addComment(bugId, comment) {
|
||||
const data = await this.request("POST", `/bug/${bugId}/comment`, { comment });
|
||||
return data.id;
|
||||
}
|
||||
async tagComment(commentId, tags) {
|
||||
await this.request("PUT", `/bug/comment/${commentId}/tags`, { add: tags });
|
||||
}
|
||||
// Product operations
|
||||
async getProduct(nameOrId) {
|
||||
const data = await this.request("GET", `/product/${encodeURIComponent(String(nameOrId))}`);
|
||||
if (!data.products?.length)
|
||||
throw new Error(`Product "${nameOrId}" not found`);
|
||||
return data.products[0];
|
||||
}
|
||||
async getAccessibleProducts() {
|
||||
const data = await this.request("GET", "/product?type=accessible");
|
||||
return data.products ?? [];
|
||||
}
|
||||
async createProduct(payload) {
|
||||
const data = await this.request("POST", "/product", payload);
|
||||
return data.id;
|
||||
}
|
||||
// Component operations
|
||||
async getComponents(product) {
|
||||
const prod = await this.getProduct(product);
|
||||
return prod.components ?? [];
|
||||
}
|
||||
async createComponent(payload) {
|
||||
const data = await this.request("POST", "/component", payload);
|
||||
return data.id;
|
||||
}
|
||||
}
|
||||
148
src/bugzilla/client.ts
Normal file
148
src/bugzilla/client.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Bugzilla 5.0+ REST API client — thin wrapper around fetch
|
||||
import type { BugzillaConfig } from "./config.js"
|
||||
import type {
|
||||
BugzillaBug,
|
||||
BugzillaComment,
|
||||
BugzillaProduct,
|
||||
BugzillaComponent,
|
||||
BugzillaSearchParams,
|
||||
BugzillaCreatePayload,
|
||||
BugzillaUpdatePayload,
|
||||
BugzillaCreateProductPayload,
|
||||
BugzillaCreateComponentPayload,
|
||||
BugzillaSearchResponse,
|
||||
BugzillaCreateResponse,
|
||||
BugzillaUpdateResponse,
|
||||
BugzillaCommentsResponse,
|
||||
BugzillaProductResponse,
|
||||
} from "./types.js"
|
||||
|
||||
export class BugzillaClient {
|
||||
private baseUrl: string
|
||||
private apiKey: string
|
||||
|
||||
constructor(config: BugzillaConfig) {
|
||||
this.baseUrl = config.baseUrl.replace(/\/+$/, "")
|
||||
this.apiKey = config.apiKey
|
||||
}
|
||||
|
||||
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
// Auth: api_key in query for GET, in body for POST/PUT
|
||||
let url: string
|
||||
if (method === "GET") {
|
||||
const sep = path.includes("?") ? "&" : "?"
|
||||
url = `${this.baseUrl}${path}${sep}api_key=${encodeURIComponent(this.apiKey)}`
|
||||
} else {
|
||||
url = `${this.baseUrl}${path}`
|
||||
const bodyObj = (body ?? {}) as Record<string, unknown>
|
||||
body = { ...bodyObj, api_key: this.apiKey }
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
throw new Error(`Bugzilla API ${method} ${path} failed (${res.status}): ${text}`)
|
||||
}
|
||||
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
async getBug(id: number): Promise<BugzillaBug> {
|
||||
const data = await this.request<BugzillaSearchResponse>("GET", `/bug/${id}`)
|
||||
if (!data.bugs?.length) throw new Error(`Bug ${id} not found`)
|
||||
return data.bugs[0]
|
||||
}
|
||||
|
||||
async searchBugs(params: BugzillaSearchParams): Promise<BugzillaBug[]> {
|
||||
const query = new URLSearchParams()
|
||||
if (params.product) query.set("product", params.product)
|
||||
if (params.component) query.set("component", params.component)
|
||||
if (params.summary) query.set("summary", params.summary)
|
||||
if (params.limit) query.set("limit", String(params.limit))
|
||||
if (params.offset) query.set("offset", String(params.offset))
|
||||
if (params.last_change_time) query.set("last_change_time", params.last_change_time)
|
||||
|
||||
if (params.status) {
|
||||
const statuses = Array.isArray(params.status) ? params.status : [params.status]
|
||||
for (const s of statuses) query.append("status", s)
|
||||
}
|
||||
|
||||
if (params.id) {
|
||||
const ids = Array.isArray(params.id) ? params.id : [params.id]
|
||||
for (const id of ids) query.append("id", String(id))
|
||||
}
|
||||
|
||||
if (params.alias) {
|
||||
const aliases = Array.isArray(params.alias) ? params.alias : [params.alias]
|
||||
for (const a of aliases) query.append("alias", a)
|
||||
}
|
||||
|
||||
if (params.url) query.set("url", params.url)
|
||||
|
||||
const qs = query.toString()
|
||||
const path = `/bug${qs ? `?${qs}` : ""}`
|
||||
const data = await this.request<BugzillaSearchResponse>("GET", path)
|
||||
return data.bugs ?? []
|
||||
}
|
||||
|
||||
async createBug(payload: BugzillaCreatePayload): Promise<number> {
|
||||
const data = await this.request<BugzillaCreateResponse>("POST", "/bug", payload)
|
||||
return data.id
|
||||
}
|
||||
|
||||
async updateBug(id: number, payload: BugzillaUpdatePayload): Promise<BugzillaUpdateResponse> {
|
||||
return this.request<BugzillaUpdateResponse>("PUT", `/bug/${id}`, payload)
|
||||
}
|
||||
|
||||
async getComments(bugId: number): Promise<BugzillaComment[]> {
|
||||
const data = await this.request<BugzillaCommentsResponse>("GET", `/bug/${bugId}/comment`)
|
||||
return data.bugs?.[String(bugId)]?.comments ?? []
|
||||
}
|
||||
|
||||
async addComment(bugId: number, comment: string): Promise<number> {
|
||||
const data = await this.request<{ id: number }>("POST", `/bug/${bugId}/comment`, { comment })
|
||||
return data.id
|
||||
}
|
||||
|
||||
async tagComment(commentId: number, tags: string[]): Promise<void> {
|
||||
await this.request("PUT", `/bug/comment/${commentId}/tags`, { add: tags })
|
||||
}
|
||||
|
||||
// Product operations
|
||||
|
||||
async getProduct(nameOrId: string | number): Promise<BugzillaProduct> {
|
||||
const data = await this.request<BugzillaProductResponse>(
|
||||
"GET",
|
||||
`/product/${encodeURIComponent(String(nameOrId))}`
|
||||
)
|
||||
if (!data.products?.length) throw new Error(`Product "${nameOrId}" not found`)
|
||||
return data.products[0]
|
||||
}
|
||||
|
||||
async getAccessibleProducts(): Promise<BugzillaProduct[]> {
|
||||
const data = await this.request<BugzillaProductResponse>("GET", "/product?type=accessible")
|
||||
return data.products ?? []
|
||||
}
|
||||
|
||||
async createProduct(payload: BugzillaCreateProductPayload): Promise<number> {
|
||||
const data = await this.request<{ id: number }>("POST", "/product", payload)
|
||||
return data.id
|
||||
}
|
||||
|
||||
// Component operations
|
||||
|
||||
async getComponents(product: string): Promise<BugzillaComponent[]> {
|
||||
const prod = await this.getProduct(product)
|
||||
return prod.components ?? []
|
||||
}
|
||||
|
||||
async createComponent(payload: BugzillaCreateComponentPayload): Promise<number> {
|
||||
const data = await this.request<{ id: number }>("POST", "/component", payload)
|
||||
return data.id
|
||||
}
|
||||
}
|
||||
7
src/bugzilla/config.d.ts
vendored
Normal file
7
src/bugzilla/config.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface BugzillaConfig {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
product: string;
|
||||
component: string;
|
||||
}
|
||||
export declare function loadConfig(cwd?: string): BugzillaConfig;
|
||||
28
src/bugzilla/config.js
Normal file
28
src/bugzilla/config.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Bugzilla configuration — loads from .bugzilla.json or environment variables
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
const CONFIG_FILE = ".bugzilla.json";
|
||||
export function loadConfig(cwd = process.cwd()) {
|
||||
const filePath = path.join(cwd, CONFIG_FILE);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const json = JSON.parse(raw);
|
||||
return {
|
||||
baseUrl: json.baseUrl ?? json.base_url ?? "",
|
||||
apiKey: json.apiKey ?? json.api_key,
|
||||
product: json.product ?? "",
|
||||
component: json.component ?? "",
|
||||
};
|
||||
}
|
||||
const baseUrl = process.env.BUGZILLA_URL;
|
||||
const apiKey = process.env.BUGZILLA_API_KEY;
|
||||
if (!baseUrl || !apiKey) {
|
||||
throw new Error(`Bugzilla config not found. Create ${CONFIG_FILE} or set BUGZILLA_URL and BUGZILLA_API_KEY`);
|
||||
}
|
||||
return {
|
||||
baseUrl: baseUrl.replace(/\/+$/, ""),
|
||||
apiKey,
|
||||
product: process.env.BUGZILLA_PRODUCT ?? "",
|
||||
component: process.env.BUGZILLA_COMPONENT ?? "",
|
||||
};
|
||||
}
|
||||
43
src/bugzilla/config.ts
Normal file
43
src/bugzilla/config.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Bugzilla configuration — loads from .bugzilla.json or environment variables
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
export interface BugzillaConfig {
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
product: string
|
||||
component: string
|
||||
}
|
||||
|
||||
const CONFIG_FILE = ".bugzilla.json"
|
||||
|
||||
export function loadConfig(cwd = process.cwd()): BugzillaConfig {
|
||||
const filePath = path.join(cwd, CONFIG_FILE)
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const raw = fs.readFileSync(filePath, "utf-8")
|
||||
const json = JSON.parse(raw)
|
||||
return {
|
||||
baseUrl: json.baseUrl ?? json.base_url ?? "",
|
||||
apiKey: json.apiKey ?? json.api_key,
|
||||
product: json.product ?? "",
|
||||
component: json.component ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BUGZILLA_URL
|
||||
const apiKey = process.env.BUGZILLA_API_KEY
|
||||
|
||||
if (!baseUrl || !apiKey) {
|
||||
throw new Error(
|
||||
`Bugzilla config not found. Create ${CONFIG_FILE} or set BUGZILLA_URL and BUGZILLA_API_KEY`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: baseUrl.replace(/\/+$/, ""),
|
||||
apiKey,
|
||||
product: process.env.BUGZILLA_PRODUCT ?? "",
|
||||
component: process.env.BUGZILLA_COMPONENT ?? "",
|
||||
}
|
||||
}
|
||||
14
src/bugzilla/fieldmap.d.ts
vendored
Normal file
14
src/bugzilla/fieldmap.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Issue, IssueType, IssueStatus, IssuePriority } from "../issue.js";
|
||||
import type { BugzillaCreatePayload, BugzillaUpdatePayload } from "./types.js";
|
||||
export declare function statusToBugzilla(status: IssueStatus): {
|
||||
status: string;
|
||||
resolution?: string;
|
||||
};
|
||||
export declare function priorityToBugzilla(priority: IssuePriority): string;
|
||||
export declare function typeToBugzilla(type: IssueType): string;
|
||||
export declare function resolveProductComponent(issue: Issue, bugzilla?: import("../tracker.js").BugzillaTracker): {
|
||||
product: string;
|
||||
component: string;
|
||||
} | null;
|
||||
export declare function issueToBugzillaCreate(issue: Issue, product: string, component: string, url?: string): BugzillaCreatePayload;
|
||||
export declare function issueToBugzillaUpdate(issue: Issue): BugzillaUpdatePayload;
|
||||
74
src/bugzilla/fieldmap.js
Normal file
74
src/bugzilla/fieldmap.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// Status mapping: TODO → Bugzilla
|
||||
const STATUS_TO_BZ = {
|
||||
"open": { status: "CONFIRMED" },
|
||||
"in-progress": { status: "IN_PROGRESS" },
|
||||
"done": { status: "RESOLVED", resolution: "FIXED" },
|
||||
"hold": { status: "RESOLVED", resolution: "LATER" },
|
||||
"cancelled": { status: "RESOLVED", resolution: "WONTFIX" },
|
||||
};
|
||||
// Priority mapping
|
||||
const PRIORITY_TO_BZ = {
|
||||
"low": "Low",
|
||||
"medium": "Normal",
|
||||
"high": "Highest",
|
||||
};
|
||||
// Type → Severity mapping
|
||||
const TYPE_TO_SEVERITY = {
|
||||
"feature": "enhancement",
|
||||
"bugfix": "normal",
|
||||
"hotfix": "critical",
|
||||
};
|
||||
export function statusToBugzilla(status) {
|
||||
return STATUS_TO_BZ[status] ?? { status: "NEW" };
|
||||
}
|
||||
export function priorityToBugzilla(priority) {
|
||||
return PRIORITY_TO_BZ[priority] ?? "Normal";
|
||||
}
|
||||
export function typeToBugzilla(type) {
|
||||
return TYPE_TO_SEVERITY[type] ?? "normal";
|
||||
}
|
||||
export function resolveProductComponent(issue, bugzilla) {
|
||||
if (!bugzilla)
|
||||
return null;
|
||||
if (issue.module) {
|
||||
const mapping = bugzilla.mappings.find((m) => m.module === issue.module);
|
||||
if (mapping)
|
||||
return { product: mapping.product, component: mapping.component };
|
||||
}
|
||||
// Fall back to first mapping
|
||||
return bugzilla.mappings.length > 0
|
||||
? { product: bugzilla.mappings[0].product, component: bugzilla.mappings[0].component }
|
||||
: null;
|
||||
}
|
||||
export function issueToBugzillaCreate(issue, product, component, url) {
|
||||
const bz = statusToBugzilla(issue.status);
|
||||
return {
|
||||
product,
|
||||
component,
|
||||
version: "unspecified",
|
||||
summary: issue.title,
|
||||
url,
|
||||
description: issue.description || undefined,
|
||||
priority: priorityToBugzilla(issue.priority),
|
||||
severity: typeToBugzilla(issue.type),
|
||||
op_sys: "All",
|
||||
rep_platform: "All",
|
||||
depends_on: issue.relationships.dependsOn ?? [],
|
||||
blocks: issue.relationships.blocks ?? [],
|
||||
see_also: (issue.relationships.relatesTo ?? []).map(String),
|
||||
};
|
||||
}
|
||||
export function issueToBugzillaUpdate(issue) {
|
||||
const bz = statusToBugzilla(issue.status);
|
||||
const payload = {
|
||||
summary: issue.title,
|
||||
status: bz.status,
|
||||
priority: priorityToBugzilla(issue.priority),
|
||||
severity: typeToBugzilla(issue.type),
|
||||
depends_on: { set: issue.relationships.dependsOn ?? [] },
|
||||
blocks: { set: issue.relationships.blocks ?? [] },
|
||||
};
|
||||
if (bz.resolution)
|
||||
payload.resolution = bz.resolution;
|
||||
return payload;
|
||||
}
|
||||
92
src/bugzilla/fieldmap.ts
Normal file
92
src/bugzilla/fieldmap.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Field mapping between MIME TODO issues and Bugzilla bugs
|
||||
import type { Issue, IssueType, IssueStatus, IssuePriority } from "../issue.js"
|
||||
import type { BugzillaCreatePayload, BugzillaUpdatePayload } from "./types.js"
|
||||
|
||||
// Status mapping: TODO → Bugzilla
|
||||
const STATUS_TO_BZ: Record<IssueStatus, { status: string; resolution?: string }> = {
|
||||
"open": { status: "CONFIRMED" },
|
||||
"in-progress": { status: "IN_PROGRESS" },
|
||||
"done": { status: "RESOLVED", resolution: "FIXED" },
|
||||
"hold": { status: "RESOLVED", resolution: "LATER" },
|
||||
"cancelled": { status: "RESOLVED", resolution: "WONTFIX" },
|
||||
}
|
||||
|
||||
// Priority mapping
|
||||
const PRIORITY_TO_BZ: Record<IssuePriority, string> = {
|
||||
"low": "Low",
|
||||
"medium": "Normal",
|
||||
"high": "Highest",
|
||||
}
|
||||
|
||||
// Type → Severity mapping
|
||||
const TYPE_TO_SEVERITY: Record<IssueType, string> = {
|
||||
"feature": "enhancement",
|
||||
"bugfix": "normal",
|
||||
"hotfix": "critical",
|
||||
}
|
||||
|
||||
export function statusToBugzilla(status: IssueStatus): { status: string; resolution?: string } {
|
||||
return STATUS_TO_BZ[status] ?? { status: "NEW" }
|
||||
}
|
||||
|
||||
export function priorityToBugzilla(priority: IssuePriority): string {
|
||||
return PRIORITY_TO_BZ[priority] ?? "Normal"
|
||||
}
|
||||
|
||||
export function typeToBugzilla(type: IssueType): string {
|
||||
return TYPE_TO_SEVERITY[type] ?? "normal"
|
||||
}
|
||||
|
||||
export function resolveProductComponent(
|
||||
issue: Issue,
|
||||
bugzilla?: import("../tracker.js").BugzillaTracker
|
||||
): { product: string; component: string } | null {
|
||||
if (!bugzilla) return null
|
||||
if (issue.module) {
|
||||
const mapping = bugzilla.mappings.find((m: { module: string; product: string; component: string }) => m.module === issue.module)
|
||||
if (mapping) return { product: mapping.product, component: mapping.component }
|
||||
}
|
||||
// Fall back to first mapping
|
||||
return bugzilla.mappings.length > 0
|
||||
? { product: bugzilla.mappings[0].product, component: bugzilla.mappings[0].component }
|
||||
: null
|
||||
}
|
||||
|
||||
export function issueToBugzillaCreate(
|
||||
issue: Issue,
|
||||
product: string,
|
||||
component: string,
|
||||
url?: string
|
||||
): BugzillaCreatePayload {
|
||||
const bz = statusToBugzilla(issue.status)
|
||||
return {
|
||||
product,
|
||||
component,
|
||||
version: "unspecified",
|
||||
summary: issue.title,
|
||||
url,
|
||||
description: issue.description || undefined,
|
||||
priority: priorityToBugzilla(issue.priority),
|
||||
severity: typeToBugzilla(issue.type),
|
||||
op_sys: "All",
|
||||
rep_platform: "All",
|
||||
depends_on: issue.relationships.dependsOn ?? [],
|
||||
blocks: issue.relationships.blocks ?? [],
|
||||
see_also: (issue.relationships.relatesTo ?? []).map(String),
|
||||
}
|
||||
}
|
||||
|
||||
export function issueToBugzillaUpdate(issue: Issue): BugzillaUpdatePayload {
|
||||
const bz = statusToBugzilla(issue.status)
|
||||
const payload: BugzillaUpdatePayload = {
|
||||
summary: issue.title,
|
||||
status: bz.status,
|
||||
priority: priorityToBugzilla(issue.priority),
|
||||
severity: typeToBugzilla(issue.type),
|
||||
depends_on: { set: issue.relationships.dependsOn ?? [] },
|
||||
blocks: { set: issue.relationships.blocks ?? [] },
|
||||
}
|
||||
if (bz.resolution) payload.resolution = bz.resolution
|
||||
return payload
|
||||
}
|
||||
|
||||
7
src/bugzilla/index.ts
Normal file
7
src/bugzilla/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Re-export all Bugzilla modules for convenient imports
|
||||
export { BugzillaClient } from "./client.js"
|
||||
export { loadConfig } from "./config.js"
|
||||
export type { BugzillaConfig } from "./config.js"
|
||||
export * from "./types.js"
|
||||
export * from "./fieldmap.js"
|
||||
export * from "./origin.js"
|
||||
6
src/bugzilla/origin.d.ts
vendored
Normal file
6
src/bugzilla/origin.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export declare function getGitRemoteUrl(cwd?: string): string;
|
||||
export declare function getGitBranch(cwd?: string): string;
|
||||
export declare function normalizeRemoteUrl(url: string): string;
|
||||
export declare function buildOriginUrl(remoteUrl: string, branch: string, todoPath: string, issueId: number): string;
|
||||
export declare function buildCommitUrl(remoteUrl: string, commitHash: string): string;
|
||||
export declare function shortHash(hash: string): string;
|
||||
70
src/bugzilla/origin.js
Normal file
70
src/bugzilla/origin.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Origin URL — builds a resolvable URL pointing to a TODO issue in the repo's web UI
|
||||
// Format varies by host:
|
||||
// Bitbucket: <base>/src/<branch>/<path>#<id>
|
||||
// GitHub: <base>/blob/<branch>/<path>#<id>
|
||||
// GitLab: <base>/-/blob/<branch>/<path>#<id>
|
||||
import { execSync } from "child_process";
|
||||
export function getGitRemoteUrl(cwd = process.cwd()) {
|
||||
try {
|
||||
return execSync("git remote get-url origin", { cwd, encoding: "utf-8" }).trim();
|
||||
}
|
||||
catch {
|
||||
throw new Error("Could not determine git remote URL. Is this a git repository with an 'origin' remote?");
|
||||
}
|
||||
}
|
||||
export function getGitBranch(cwd = process.cwd()) {
|
||||
try {
|
||||
return execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8" }).trim();
|
||||
}
|
||||
catch {
|
||||
return "main";
|
||||
}
|
||||
}
|
||||
export function normalizeRemoteUrl(url) {
|
||||
let normalized = url;
|
||||
const sshMatch = normalized.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
||||
if (sshMatch) {
|
||||
normalized = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
||||
}
|
||||
normalized = normalized.replace(/\.git$/, "");
|
||||
normalized = normalized.replace(/\/+$/, "");
|
||||
return normalized;
|
||||
}
|
||||
function detectHost(url) {
|
||||
if (url.includes("bitbucket.org"))
|
||||
return "bitbucket";
|
||||
if (url.includes("github.com"))
|
||||
return "github";
|
||||
if (url.includes("gitlab.com") || url.includes("gitlab"))
|
||||
return "gitlab";
|
||||
return "unknown";
|
||||
}
|
||||
export function buildOriginUrl(remoteUrl, branch, todoPath, issueId) {
|
||||
const base = normalizeRemoteUrl(remoteUrl);
|
||||
const host = detectHost(base);
|
||||
switch (host) {
|
||||
case "github":
|
||||
return `${base}/blob/${branch}/${todoPath}#${issueId}`;
|
||||
case "gitlab":
|
||||
return `${base}/-/blob/${branch}/${todoPath}#${issueId}`;
|
||||
case "bitbucket":
|
||||
default:
|
||||
return `${base}/src/${branch}/${todoPath}#${issueId}`;
|
||||
}
|
||||
}
|
||||
export function buildCommitUrl(remoteUrl, commitHash) {
|
||||
const base = normalizeRemoteUrl(remoteUrl);
|
||||
const host = detectHost(base);
|
||||
switch (host) {
|
||||
case "github":
|
||||
return `${base}/commit/${commitHash}`;
|
||||
case "gitlab":
|
||||
return `${base}/-/commit/${commitHash}`;
|
||||
case "bitbucket":
|
||||
default:
|
||||
return `${base}/commits/${commitHash}`;
|
||||
}
|
||||
}
|
||||
export function shortHash(hash) {
|
||||
return hash.slice(0, 7);
|
||||
}
|
||||
80
src/bugzilla/origin.ts
Normal file
80
src/bugzilla/origin.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Origin URL — builds a resolvable URL pointing to a TODO issue in the repo's web UI
|
||||
// Format varies by host:
|
||||
// Bitbucket: <base>/src/<branch>/<path>#<id>
|
||||
// GitHub: <base>/blob/<branch>/<path>#<id>
|
||||
// GitLab: <base>/-/blob/<branch>/<path>#<id>
|
||||
|
||||
import { execSync } from "child_process"
|
||||
|
||||
export function getGitRemoteUrl(cwd = process.cwd()): string {
|
||||
try {
|
||||
return execSync("git remote get-url origin", { cwd, encoding: "utf-8" }).trim()
|
||||
} catch {
|
||||
throw new Error("Could not determine git remote URL. Is this a git repository with an 'origin' remote?")
|
||||
}
|
||||
}
|
||||
|
||||
export function getGitBranch(cwd = process.cwd()): string {
|
||||
try {
|
||||
return execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8" }).trim()
|
||||
} catch {
|
||||
return "main"
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeRemoteUrl(url: string): string {
|
||||
let normalized = url
|
||||
const sshMatch = normalized.match(/^git@([^:]+):(.+?)(?:\.git)?$/)
|
||||
if (sshMatch) {
|
||||
normalized = `https://${sshMatch[1]}/${sshMatch[2]}`
|
||||
}
|
||||
normalized = normalized.replace(/\.git$/, "")
|
||||
normalized = normalized.replace(/\/+$/, "")
|
||||
return normalized
|
||||
}
|
||||
|
||||
function detectHost(url: string): "bitbucket" | "github" | "gitlab" | "unknown" {
|
||||
if (url.includes("bitbucket.org")) return "bitbucket"
|
||||
if (url.includes("github.com")) return "github"
|
||||
if (url.includes("gitlab.com") || url.includes("gitlab")) return "gitlab"
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
export function buildOriginUrl(
|
||||
remoteUrl: string,
|
||||
branch: string,
|
||||
todoPath: string,
|
||||
issueId: number
|
||||
): string {
|
||||
const base = normalizeRemoteUrl(remoteUrl)
|
||||
const host = detectHost(base)
|
||||
|
||||
switch (host) {
|
||||
case "github":
|
||||
return `${base}/blob/${branch}/${todoPath}#${issueId}`
|
||||
case "gitlab":
|
||||
return `${base}/-/blob/${branch}/${todoPath}#${issueId}`
|
||||
case "bitbucket":
|
||||
default:
|
||||
return `${base}/src/${branch}/${todoPath}#${issueId}`
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCommitUrl(remoteUrl: string, commitHash: string): string {
|
||||
const base = normalizeRemoteUrl(remoteUrl)
|
||||
const host = detectHost(base)
|
||||
|
||||
switch (host) {
|
||||
case "github":
|
||||
return `${base}/commit/${commitHash}`
|
||||
case "gitlab":
|
||||
return `${base}/-/commit/${commitHash}`
|
||||
case "bitbucket":
|
||||
default:
|
||||
return `${base}/commits/${commitHash}`
|
||||
}
|
||||
}
|
||||
|
||||
export function shortHash(hash: string): string {
|
||||
return hash.slice(0, 7)
|
||||
}
|
||||
125
src/bugzilla/types.d.ts
vendored
Normal file
125
src/bugzilla/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
export interface BugzillaBug {
|
||||
id: number;
|
||||
alias: string[];
|
||||
summary: string;
|
||||
status: string;
|
||||
resolution: string;
|
||||
priority: string;
|
||||
severity: string;
|
||||
product: string;
|
||||
component: string;
|
||||
description?: string;
|
||||
creation_time: string;
|
||||
last_change_time: string;
|
||||
depends_on: number[];
|
||||
blocks: number[];
|
||||
url: string;
|
||||
see_also: string[];
|
||||
assigned_to: string;
|
||||
creator: string;
|
||||
is_open: boolean;
|
||||
}
|
||||
export interface BugzillaComment {
|
||||
id: number;
|
||||
bug_id: number;
|
||||
text: string;
|
||||
creator: string;
|
||||
time: string;
|
||||
count: number;
|
||||
is_private: boolean;
|
||||
tags: string[];
|
||||
}
|
||||
export interface BugzillaSearchParams {
|
||||
product?: string;
|
||||
component?: string;
|
||||
status?: string | string[];
|
||||
id?: number | number[];
|
||||
alias?: string | string[];
|
||||
url?: string;
|
||||
summary?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
last_change_time?: string;
|
||||
}
|
||||
export interface BugzillaCreatePayload {
|
||||
product: string;
|
||||
component: string;
|
||||
summary: string;
|
||||
version?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
severity?: string;
|
||||
op_sys?: string;
|
||||
rep_platform?: string;
|
||||
depends_on?: number[];
|
||||
blocks?: number[];
|
||||
see_also?: string[];
|
||||
}
|
||||
export interface BugzillaUpdatePayload {
|
||||
summary?: string;
|
||||
status?: string;
|
||||
resolution?: string;
|
||||
priority?: string;
|
||||
severity?: string;
|
||||
depends_on?: {
|
||||
set: number[];
|
||||
};
|
||||
blocks?: {
|
||||
set: number[];
|
||||
};
|
||||
see_also?: {
|
||||
add?: string[];
|
||||
remove?: string[];
|
||||
};
|
||||
}
|
||||
export interface BugzillaSearchResponse {
|
||||
bugs: BugzillaBug[];
|
||||
}
|
||||
export interface BugzillaCreateResponse {
|
||||
id: number;
|
||||
}
|
||||
export interface BugzillaUpdateResponse {
|
||||
bugs: Array<{
|
||||
id: number;
|
||||
changes: Record<string, {
|
||||
removed: string;
|
||||
added: string;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
export interface BugzillaCommentsResponse {
|
||||
bugs: Record<string, {
|
||||
comments: BugzillaComment[];
|
||||
}>;
|
||||
}
|
||||
export interface BugzillaComponent {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
default_assigned_to: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
export interface BugzillaProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
is_active: boolean;
|
||||
components: BugzillaComponent[];
|
||||
}
|
||||
export interface BugzillaProductResponse {
|
||||
products: BugzillaProduct[];
|
||||
}
|
||||
export interface BugzillaCreateProductPayload {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
is_open?: boolean;
|
||||
}
|
||||
export interface BugzillaCreateComponentPayload {
|
||||
product: string;
|
||||
name: string;
|
||||
description: string;
|
||||
default_assignee: string;
|
||||
}
|
||||
2
src/bugzilla/types.js
Normal file
2
src/bugzilla/types.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Bugzilla 5.0+ REST API type definitions
|
||||
export {};
|
||||
130
src/bugzilla/types.ts
Normal file
130
src/bugzilla/types.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Bugzilla 5.0+ REST API type definitions
|
||||
|
||||
export interface BugzillaBug {
|
||||
id: number
|
||||
alias: string[]
|
||||
summary: string
|
||||
status: string
|
||||
resolution: string
|
||||
priority: string
|
||||
severity: string
|
||||
product: string
|
||||
component: string
|
||||
description?: string // comment 0 text (not always in bug response)
|
||||
creation_time: string
|
||||
last_change_time: string
|
||||
depends_on: number[]
|
||||
blocks: number[]
|
||||
url: string
|
||||
see_also: string[]
|
||||
assigned_to: string
|
||||
creator: string
|
||||
is_open: boolean
|
||||
}
|
||||
|
||||
export interface BugzillaComment {
|
||||
id: number
|
||||
bug_id: number
|
||||
text: string
|
||||
creator: string
|
||||
time: string
|
||||
count: number
|
||||
is_private: boolean
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface BugzillaSearchParams {
|
||||
product?: string
|
||||
component?: string
|
||||
status?: string | string[]
|
||||
id?: number | number[]
|
||||
alias?: string | string[]
|
||||
url?: string
|
||||
summary?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
last_change_time?: string
|
||||
}
|
||||
|
||||
export interface BugzillaCreatePayload {
|
||||
product: string
|
||||
component: string
|
||||
summary: string
|
||||
version?: string
|
||||
url?: string
|
||||
description?: string
|
||||
status?: string
|
||||
priority?: string
|
||||
severity?: string
|
||||
op_sys?: string
|
||||
rep_platform?: string
|
||||
depends_on?: number[]
|
||||
blocks?: number[]
|
||||
see_also?: string[]
|
||||
}
|
||||
|
||||
export interface BugzillaUpdatePayload {
|
||||
summary?: string
|
||||
status?: string
|
||||
resolution?: string
|
||||
priority?: string
|
||||
severity?: string
|
||||
depends_on?: { set: number[] }
|
||||
blocks?: { set: number[] }
|
||||
see_also?: { add?: string[]; remove?: string[] }
|
||||
}
|
||||
|
||||
export interface BugzillaSearchResponse {
|
||||
bugs: BugzillaBug[]
|
||||
}
|
||||
|
||||
export interface BugzillaCreateResponse {
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface BugzillaUpdateResponse {
|
||||
bugs: Array<{
|
||||
id: number
|
||||
changes: Record<string, { removed: string; added: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
export interface BugzillaCommentsResponse {
|
||||
bugs: Record<string, { comments: BugzillaComment[] }>
|
||||
}
|
||||
|
||||
// Product and component types
|
||||
|
||||
export interface BugzillaComponent {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
default_assigned_to: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
export interface BugzillaProduct {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
is_active: boolean
|
||||
components: BugzillaComponent[]
|
||||
}
|
||||
|
||||
export interface BugzillaProductResponse {
|
||||
products: BugzillaProduct[]
|
||||
}
|
||||
|
||||
export interface BugzillaCreateProductPayload {
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
is_open?: boolean
|
||||
}
|
||||
|
||||
export interface BugzillaCreateComponentPayload {
|
||||
product: string
|
||||
name: string
|
||||
description: string
|
||||
default_assignee: string
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue