|
3 | 3 | namespace Utopia\VCS\Adapter\Git; |
4 | 4 |
|
5 | 5 | use Exception; |
| 6 | +use Utopia\VCS\Exception\RepositoryNotFound; |
6 | 7 |
|
7 | 8 | class Gogs extends Gitea |
8 | 9 | { |
@@ -63,18 +64,34 @@ public function createOrganization(string $orgName): string |
63 | 64 | /** |
64 | 65 | * Search repositories in organization |
65 | 66 | * |
66 | | - * Gogs requires the `q` parameter for search to return results. |
67 | | - * When no search query is given, we pass '*' as a wildcard. |
| 67 | + * When no search query is given, Gogs search API returns empty results, |
| 68 | + * so we fall back to listing org repos directly via /orgs/{org}/repos. |
68 | 69 | * |
69 | 70 | * @return array<mixed> |
70 | 71 | */ |
71 | 72 | public function searchRepositories(string $owner, int $page, int $per_page, string $search = ''): array |
72 | 73 | { |
73 | | - if (empty($search)) { |
74 | | - $search = '_'; // Gogs requires q param; underscore matches most repo names |
| 74 | + if (!empty($search)) { |
| 75 | + return parent::searchRepositories($owner, $page, $per_page, $search); |
75 | 76 | } |
76 | 77 |
|
77 | | - return parent::searchRepositories($owner, $page, $per_page, $search); |
| 78 | + // List all repos for the org directly |
| 79 | + $url = "/orgs/{$owner}/repos"; |
| 80 | + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); |
| 81 | + |
| 82 | + $responseBody = $response['body'] ?? []; |
| 83 | + if (!is_array($responseBody)) { |
| 84 | + $responseBody = []; |
| 85 | + } |
| 86 | + |
| 87 | + $total = count($responseBody); |
| 88 | + $offset = ($page - 1) * $per_page; |
| 89 | + $pagedRepos = array_slice($responseBody, $offset, $per_page); |
| 90 | + |
| 91 | + return [ |
| 92 | + 'items' => $pagedRepos, |
| 93 | + 'total' => $total, |
| 94 | + ]; |
78 | 95 | } |
79 | 96 |
|
80 | 97 | /** |
@@ -118,21 +135,71 @@ public function getRepositoryTree(string $owner, string $repositoryName, string |
118 | 135 | /** |
119 | 136 | * Get repository name by ID |
120 | 137 | * |
121 | | - * Gogs does not support /repositories/{id}. Uses search as fallback. |
| 138 | + * Gogs does not have /repositories/{id}. Searches all repos to find by ID. |
122 | 139 | */ |
123 | 140 | public function getRepositoryName(string $repositoryId): string |
124 | 141 | { |
125 | | - throw new Exception("getRepositoryName by ID is not supported by Gogs"); |
| 142 | + $repo = $this->findRepositoryById((int) $repositoryId); |
| 143 | + |
| 144 | + return $repo['name']; |
126 | 145 | } |
127 | 146 |
|
128 | 147 | /** |
129 | | - * Get owner name |
| 148 | + * Get owner name by repository ID |
130 | 149 | * |
131 | | - * Gogs does not support /repositories/{id}. |
| 150 | + * Gogs does not have /repositories/{id}. Searches all repos to find by ID. |
132 | 151 | */ |
133 | 152 | public function getOwnerName(string $installationId, ?int $repositoryId = null): string |
134 | 153 | { |
135 | | - throw new Exception("getOwnerName by repository ID is not supported by Gogs"); |
| 154 | + if ($repositoryId === null || $repositoryId <= 0) { |
| 155 | + throw new Exception("repositoryId is required for this adapter"); |
| 156 | + } |
| 157 | + |
| 158 | + $repo = $this->findRepositoryById($repositoryId); |
| 159 | + $owner = $repo['owner'] ?? []; |
| 160 | + |
| 161 | + if (empty($owner['login'])) { |
| 162 | + throw new Exception("Owner login missing or empty in response"); |
| 163 | + } |
| 164 | + |
| 165 | + return $owner['login']; |
| 166 | + } |
| 167 | + |
| 168 | + /** |
| 169 | + * Find a repository by its numeric ID using the search API. |
| 170 | + * |
| 171 | + * @return array<mixed> Repository data |
| 172 | + */ |
| 173 | + private function findRepositoryById(int $repositoryId): array |
| 174 | + { |
| 175 | + $page = 1; |
| 176 | + $limit = 50; |
| 177 | + |
| 178 | + while ($page <= 100) { |
| 179 | + $url = "/repos/search?q=_&limit={$limit}&page={$page}"; |
| 180 | + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); |
| 181 | + |
| 182 | + $responseBody = $response['body'] ?? []; |
| 183 | + $repos = $responseBody['data'] ?? []; |
| 184 | + |
| 185 | + if (empty($repos)) { |
| 186 | + break; |
| 187 | + } |
| 188 | + |
| 189 | + foreach ($repos as $repo) { |
| 190 | + if (($repo['id'] ?? 0) === $repositoryId) { |
| 191 | + return $repo; |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + if (count($repos) < $limit) { |
| 196 | + break; |
| 197 | + } |
| 198 | + |
| 199 | + $page++; |
| 200 | + } |
| 201 | + |
| 202 | + throw new RepositoryNotFound("Repository not found"); |
136 | 203 | } |
137 | 204 |
|
138 | 205 | /** |
@@ -190,41 +257,34 @@ public function getLatestCommit(string $owner, string $repositoryName, string $b |
190 | 257 | /** |
191 | 258 | * Create a file in a repository |
192 | 259 | * |
193 | | - * Gogs PUT /contents/{path} only works on the default branch and cannot |
194 | | - * target a specific branch. When a branch is specified we fall back to |
195 | | - * git CLI so the file lands on the correct branch. |
| 260 | + * Gogs uses PUT /repos/{owner}/{repo}/contents/{path}. |
| 261 | + * For non-default branches we use git CLI, because the Gogs API `branch` |
| 262 | + * param creates a new branch rather than targeting an existing one. |
196 | 263 | * |
197 | 264 | * @return array<mixed> |
198 | 265 | */ |
199 | 266 | public function createFile(string $owner, string $repositoryName, string $filepath, string $content, string $message = 'Add file', string $branch = ''): array |
200 | 267 | { |
201 | | - if (empty($branch)) { |
202 | | - // Default branch — use Gogs API (PUT) |
203 | | - $url = "/repos/{$owner}/{$repositoryName}/contents/{$filepath}"; |
204 | | - |
205 | | - $response = $this->call( |
206 | | - self::METHOD_PUT, |
207 | | - $url, |
208 | | - ['Authorization' => "token $this->accessToken"], |
209 | | - [ |
210 | | - 'content' => base64_encode($content), |
211 | | - 'message' => $message, |
212 | | - ] |
213 | | - ); |
214 | | - |
215 | | - $responseHeaders = $response['headers'] ?? []; |
216 | | - $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; |
217 | | - if ($responseHeadersStatusCode >= 400) { |
218 | | - throw new Exception("Failed to create file {$filepath}: HTTP {$responseHeadersStatusCode}"); |
219 | | - } |
| 268 | + $url = "/repos/{$owner}/{$repositoryName}/contents/{$filepath}"; |
| 269 | + |
| 270 | + $response = $this->call( |
| 271 | + self::METHOD_PUT, |
| 272 | + $url, |
| 273 | + ['Authorization' => "token $this->accessToken"], |
| 274 | + [ |
| 275 | + 'content' => base64_encode($content), |
| 276 | + 'message' => $message, |
| 277 | + 'branch' => $branch |
| 278 | + ] |
| 279 | + ); |
220 | 280 |
|
221 | | - return $response['body'] ?? []; |
| 281 | + $responseHeaders = $response['headers'] ?? []; |
| 282 | + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; |
| 283 | + if ($responseHeadersStatusCode >= 400) { |
| 284 | + throw new Exception("Failed to create file {$filepath}: HTTP {$responseHeadersStatusCode}"); |
222 | 285 | } |
223 | 286 |
|
224 | | - // Specific branch — use git CLI |
225 | | - $this->gitCreateFile($owner, $repositoryName, $branch, $filepath, $content, $message); |
226 | | - |
227 | | - return []; |
| 287 | + return $response['body'] ?? []; |
228 | 288 | } |
229 | 289 |
|
230 | 290 | /** |
@@ -269,30 +329,6 @@ private function gitClone(string $owner, string $repositoryName, string $branch |
269 | 329 | return trim($dir, "'\""); |
270 | 330 | } |
271 | 331 |
|
272 | | - /** |
273 | | - * Create a file via git CLI: clone, write, commit, push. |
274 | | - */ |
275 | | - private function gitCreateFile(string $owner, string $repositoryName, string $branch, string $filepath, string $content, string $message): void |
276 | | - { |
277 | | - $dir = $this->gitClone($owner, $repositoryName, $branch); |
278 | | - |
279 | | - try { |
280 | | - $fullPath = $dir . '/' . $filepath; |
281 | | - $parentDir = dirname($fullPath); |
282 | | - |
283 | | - if (!is_dir($parentDir)) { |
284 | | - mkdir($parentDir, 0777, true); |
285 | | - } |
286 | | - |
287 | | - file_put_contents($fullPath, $content); |
288 | | - |
289 | | - $this->exec("git -C " . escapeshellarg($dir) . " add " . escapeshellarg($filepath)); |
290 | | - $this->exec("git -C " . escapeshellarg($dir) . " commit -m " . escapeshellarg($message)); |
291 | | - $this->exec("git -C " . escapeshellarg($dir) . " push origin " . escapeshellarg($branch)); |
292 | | - } finally { |
293 | | - $this->exec("rm -rf " . escapeshellarg($dir)); |
294 | | - } |
295 | | - } |
296 | 332 |
|
297 | 333 | /** |
298 | 334 | * Execute a shell command and throw on failure. |
|
0 commit comments