feat: add git API layer with Gitea and GitHub implementations
GitHost interface for reading/writing vault files via REST API. Gitea and GitHub implementations handle base64 content encoding, SHA-based updates, and directory listing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
extension/src/service-worker/github.ts
Normal file
108
extension/src/service-worker/github.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { GitHost } from './git-host';
|
||||
import { uint8ArrayToBase64, base64ToUint8Array } from './git-host';
|
||||
|
||||
/// GitHub Contents API implementation.
|
||||
///
|
||||
/// Endpoints:
|
||||
/// GET https://api.github.com/repos/{repoPath}/contents/{path}
|
||||
/// PUT https://api.github.com/repos/{repoPath}/contents/{path} (create/update)
|
||||
/// DELETE https://api.github.com/repos/{repoPath}/contents/{path}
|
||||
///
|
||||
/// Auth: `Bearer {apiToken}` header.
|
||||
/// Content is base64-encoded. Updates and deletes require the current file SHA.
|
||||
|
||||
export class GitHubHost implements GitHost {
|
||||
private baseUrl: string;
|
||||
private headers: Record<string, string>;
|
||||
|
||||
constructor(repoPath: string, apiToken: string) {
|
||||
this.baseUrl = `https://api.github.com/repos/${repoPath}/contents`;
|
||||
this.headers = {
|
||||
'Authorization': `Bearer ${apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
};
|
||||
}
|
||||
|
||||
async readFile(path: string): Promise<Uint8Array> {
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`GitHub readFile ${path}: ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
const json = await resp.json();
|
||||
const clean = (json.content as string).replace(/\n/g, '');
|
||||
return base64ToUint8Array(clean);
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: Uint8Array, message: string): Promise<void> {
|
||||
const b64 = uint8ArrayToBase64(content);
|
||||
|
||||
// Try to get the current SHA for an update; if 404 it's a create.
|
||||
let sha: string | null = null;
|
||||
try {
|
||||
const existing = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (existing.ok) {
|
||||
const json = await existing.json();
|
||||
sha = json.sha as string;
|
||||
}
|
||||
} catch {
|
||||
// File does not exist — will create.
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = { content: b64, message };
|
||||
if (sha) {
|
||||
body.sha = sha;
|
||||
}
|
||||
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
method: 'PUT',
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`GitHub writeFile ${path}: ${resp.status} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(path: string, message: string): Promise<void> {
|
||||
const existing = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (!existing.ok) {
|
||||
throw new Error(`GitHub deleteFile ${path}: file not found (${existing.status})`);
|
||||
}
|
||||
const json = await existing.json();
|
||||
const sha = json.sha as string;
|
||||
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({ message, sha }),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`GitHub deleteFile ${path}: ${resp.status} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async listDir(path: string): Promise<string[]> {
|
||||
const resp = await fetch(`${this.baseUrl}/${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 404) return [];
|
||||
throw new Error(`GitHub listDir ${path}: ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
const json = await resp.json();
|
||||
if (!Array.isArray(json)) return [];
|
||||
return json.map((item: { name: string }) => item.name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user