| 1 | /** |
| 2 | * BridgeService provides read access to repos via grove-bridge (Mononoke HTTP API). |
| 3 | * Replaces the old GitService that used bare git clones + shell commands. |
| 4 | */ |
| 5 | export class BridgeService { |
| 6 | constructor(private bridgeUrl: string) {} |
| 7 | |
| 8 | async listTree( |
| 9 | _owner: string, |
| 10 | repo: string, |
| 11 | ref: string, |
| 12 | path: string = "" |
| 13 | ): Promise<TreeEntry[]> { |
| 14 | try { |
| 15 | const url = path |
| 16 | ? `${this.bridgeUrl}/repos/${repo}/tree/${ref}/${path}` |
| 17 | : `${this.bridgeUrl}/repos/${repo}/tree/${ref}/`; |
| 18 | const res = await fetch(url); |
| 19 | if (!res.ok) return []; |
| 20 | const data = await res.json(); |
| 21 | return (data.entries ?? []).map((e: any) => ({ |
| 22 | name: e.name, |
| 23 | type: e.type as "blob" | "tree", |
| 24 | mode: e.type === "tree" ? "040000" : "100644", |
| 25 | hash: "", |
| 26 | })); |
| 27 | } catch { |
| 28 | return []; |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | async getBlob( |
| 33 | _owner: string, |
| 34 | repo: string, |
| 35 | ref: string, |
| 36 | path: string |
| 37 | ): Promise<{ content: string; size: number } | null> { |
| 38 | try { |
| 39 | const res = await fetch( |
| 40 | `${this.bridgeUrl}/repos/${repo}/blob/${ref}/${path}` |
| 41 | ); |
| 42 | if (!res.ok) return null; |
| 43 | const data = await res.json(); |
| 44 | return { content: data.content, size: data.size }; |
| 45 | } catch { |
| 46 | return null; |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | async getCommits( |
| 51 | _owner: string, |
| 52 | repo: string, |
| 53 | ref: string, |
| 54 | options: { path?: string; limit?: number; offset?: number } = {} |
| 55 | ): Promise<CommitInfo[]> { |
| 56 | try { |
| 57 | const limit = options.limit ?? 30; |
| 58 | const res = await fetch( |
| 59 | `${this.bridgeUrl}/repos/${repo}/commit/${ref}/history?limit=${limit}` |
| 60 | ); |
| 61 | if (!res.ok) return []; |
| 62 | const data = await res.json(); |
| 63 | const commits = (data.commits ?? []).map((c: any) => ({ |
| 64 | hash: c.hash, |
| 65 | author: c.author, |
| 66 | email: "", |
| 67 | timestamp: c.timestamp, |
| 68 | subject: (c.message ?? "").split("\n")[0], |
| 69 | body: (c.message ?? "").split("\n").slice(1).join("\n").trim(), |
| 70 | parents: c.parents ?? [], |
| 71 | })); |
| 72 | const offset = options.offset ?? 0; |
| 73 | return offset > 0 ? commits.slice(offset) : commits; |
| 74 | } catch { |
| 75 | return []; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | async getBlame( |
| 80 | _owner: string, |
| 81 | repo: string, |
| 82 | ref: string, |
| 83 | path: string |
| 84 | ): Promise<BlameLine[]> { |
| 85 | try { |
| 86 | const res = await fetch( |
| 87 | `${this.bridgeUrl}/repos/${repo}/blame/${ref}/${path}` |
| 88 | ); |
| 89 | if (!res.ok) return []; |
| 90 | const data = await res.json(); |
| 91 | return (data.blame ?? []).map((b: any) => ({ |
| 92 | hash: b.hash, |
| 93 | originalLine: b.original_line, |
| 94 | author: b.author, |
| 95 | timestamp: b.timestamp, |
| 96 | summary: "", |
| 97 | content: b.content, |
| 98 | })); |
| 99 | } catch { |
| 100 | return []; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | async getDiff( |
| 105 | _owner: string, |
| 106 | repo: string, |
| 107 | base: string, |
| 108 | head: string |
| 109 | ): Promise<{ base: string; head: string; diffs: any[] }> { |
| 110 | try { |
| 111 | const res = await fetch( |
| 112 | `${this.bridgeUrl}/repos/${repo}/diff/${base}/${head}` |
| 113 | ); |
| 114 | if (!res.ok) return { base, head, diffs: [] }; |
| 115 | const data = await res.json(); |
| 116 | return { base: data.base ?? base, head: data.head ?? head, diffs: data.diffs ?? [] }; |
| 117 | } catch { |
| 118 | return { base, head, diffs: [] }; |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | async getBranches(_owner: string, repo: string): Promise<BranchInfo[]> { |
| 123 | try { |
| 124 | const res = await fetch( |
| 125 | `${this.bridgeUrl}/repos/${repo}/bookmarks` |
| 126 | ); |
| 127 | if (!res.ok) return []; |
| 128 | const data = await res.json(); |
| 129 | return (data.bookmarks ?? []).map((b: any) => ({ |
| 130 | name: b.name, |
| 131 | hash: b.commit_id, |
| 132 | timestamp: 0, |
| 133 | subject: "", |
| 134 | })); |
| 135 | } catch { |
| 136 | return []; |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | async getReadme( |
| 141 | owner: string, |
| 142 | repo: string, |
| 143 | ref: string |
| 144 | ): Promise<string | null> { |
| 145 | const readmeNames = [ |
| 146 | "README.md", |
| 147 | "README.markdown", |
| 148 | "README.txt", |
| 149 | "README", |
| 150 | "readme.md", |
| 151 | ]; |
| 152 | |
| 153 | for (const name of readmeNames) { |
| 154 | const blob = await this.getBlob(owner, repo, ref, name); |
| 155 | if (blob) return blob.content; |
| 156 | } |
| 157 | return null; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | // Types |
| 162 | |
| 163 | export interface TreeEntry { |
| 164 | mode: string; |
| 165 | type: "blob" | "tree"; |
| 166 | hash: string; |
| 167 | name: string; |
| 168 | } |
| 169 | |
| 170 | export interface CommitInfo { |
| 171 | hash: string; |
| 172 | author: string; |
| 173 | email: string; |
| 174 | timestamp: number; |
| 175 | subject: string; |
| 176 | body: string; |
| 177 | parents: string[]; |
| 178 | } |
| 179 | |
| 180 | export interface BlameLine { |
| 181 | hash: string; |
| 182 | originalLine: number; |
| 183 | author: string; |
| 184 | timestamp: number; |
| 185 | summary: string; |
| 186 | content: string; |
| 187 | } |
| 188 | |
| 189 | export interface BranchInfo { |
| 190 | name: string; |
| 191 | hash: string; |
| 192 | timestamp: number; |
| 193 | subject: string; |
| 194 | } |
| 195 | |