Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 220 additions & 9 deletions src/VCS/Adapter/Git/GitLab.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,42 @@ private function getNamespaceId(string $owner): string
return $owner;
}

private function findMrIidForNote(string $projectPath, string $commentId): ?int
{
$page = 1;
while (true) {
$url = "/projects/{$projectPath}/merge_requests?state=all&per_page=100&page={$page}";
$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);
$responseHeaders = $response['headers'] ?? [];
$statusCode = $responseHeaders['status-code'] ?? 0;
if ($statusCode >= 400) {
throw new Exception("Failed to list merge requests: HTTP {$statusCode}");
}

$mrs = $response['body'] ?? [];

if (empty($mrs) || !is_array($mrs)) {
Comment thread
jaysomani marked this conversation as resolved.
break;
}

foreach ($mrs as $mr) {
$mrIid = $mr['iid'] ?? 0;
$noteUrl = "/projects/{$projectPath}/merge_requests/{$mrIid}/notes/{$commentId}";
$noteResponse = $this->call(self::METHOD_GET, $noteUrl, ['PRIVATE-TOKEN' => $this->accessToken]);
if (($noteResponse['headers']['status-code'] ?? 0) === 200) {
return $mrIid;
}
}

if (count($mrs) < 100) {
break;
}
$page++;
}

return null;
}

public function createRepository(string $owner, string $repositoryName, bool $private): array
{
$namespaceId = (int) $this->getNamespaceId($owner);
Expand Down Expand Up @@ -465,22 +501,81 @@ public function createWebhook(string $owner, string $repositoryName, string $url

public function createComment(string $owner, string $repositoryName, int $pullRequestNumber, string $comment): string
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$url = "/projects/{$projectPath}/merge_requests/{$pullRequestNumber}/notes";

$response = $this->call(self::METHOD_POST, $url, ['PRIVATE-TOKEN' => $this->accessToken], ['body' => $comment]);

$responseHeaders = $response['headers'] ?? [];
$statusCode = $responseHeaders['status-code'] ?? 0;
if ($statusCode >= 400) {
throw new Exception("Failed to create comment: HTTP {$statusCode}");
}

$responseBody = $response['body'] ?? [];
if (!array_key_exists('id', $responseBody)) {
throw new Exception("Comment creation response is missing comment ID.");
}

return (string) ($responseBody['id'] ?? '');
}

public function getComment(string $owner, string $repositoryName, string $commentId): string
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");

$mrIid = $this->findMrIidForNote($projectPath, $commentId);
if ($mrIid === null) {
return '';
}

$noteUrl = "/projects/{$projectPath}/merge_requests/{$mrIid}/notes/{$commentId}";
$noteResponse = $this->call(self::METHOD_GET, $noteUrl, ['PRIVATE-TOKEN' => $this->accessToken]);
return $noteResponse['body']['body'] ?? '';
}

public function updateComment(string $owner, string $repositoryName, int $commentId, string $comment): string
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");

$mrIid = $this->findMrIidForNote($projectPath, (string) $commentId);
if ($mrIid === null) {
throw new Exception("Failed to update comment: comment ID {$commentId} not found in any merge request.");
}

$noteUrl = "/projects/{$projectPath}/merge_requests/{$mrIid}/notes/{$commentId}";
$noteResponse = $this->call(self::METHOD_PUT, $noteUrl, ['PRIVATE-TOKEN' => $this->accessToken], ['body' => $comment]);
$noteHeaders = $noteResponse['headers'] ?? [];
if (($noteHeaders['status-code'] ?? 0) !== 200) {
throw new Exception("Failed to update comment: HTTP " . ($noteHeaders['status-code'] ?? 0));
}

return (string) $commentId;
}

public function getUser(string $username): array
{
throw new Exception("Not implemented");
$url = "/users?username=" . rawurlencode($username);

$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$statusCode = $responseHeaders['status-code'] ?? 0;
if ($statusCode >= 400) {
throw new Exception("Failed to get user: HTTP {$statusCode}");
}

$body = $response['body'] ?? [];

// GitLab returns an array of users — return first match
if (empty($body[0])) {
throw new Exception("User not found: {$username}");
}

return $body[0];
}

public function getOwnerName(string $installationId, ?int $repositoryId = null): string
Expand Down Expand Up @@ -511,17 +606,123 @@ public function getOwnerName(string $installationId, ?int $repositoryId = null):

public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$url = "/projects/{$projectPath}/merge_requests/{$pullRequestNumber}";

$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$statusCode = $responseHeaders['status-code'] ?? 0;
if ($statusCode >= 400) {
throw new Exception("Failed to get merge request: HTTP {$statusCode}");
}

$mr = $response['body'] ?? [];

// Normalize to match expected shape (consistent with Gitea/GitHub)
return [
'number' => $mr['iid'] ?? 0,
'title' => $mr['title'] ?? '',
'state' => $mr['state'] ?? '',
Comment thread
jaysomani marked this conversation as resolved.
'head' => [
'ref' => $mr['source_branch'] ?? '',
'sha' => $mr['sha'] ?? '',
],
'base' => [
'ref' => $mr['target_branch'] ?? '',
],
];
}

public function getPullRequestFiles(string $owner, string $repositoryName, int $pullRequestNumber): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");

// Poll until diff is ready (patch_id_sha not null)
$maxAttempts = 10;
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
$mrResponse = $this->call(
self::METHOD_GET,
"/projects/{$projectPath}/merge_requests/{$pullRequestNumber}",
['PRIVATE-TOKEN' => $this->accessToken]
);
$mrBody = $mrResponse['body'] ?? [];
if (($mrBody['patch_id_sha'] ?? null) !== null) {
break;
}
usleep(1000000); // 1 second
}
Comment thread
jaysomani marked this conversation as resolved.

// Fetch diffs with pagination
$allFiles = [];
$page = 1;
$perPage = 100;

while (true) {
$url = "/projects/{$projectPath}/merge_requests/{$pullRequestNumber}/diffs?page={$page}&per_page={$perPage}";
$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$statusCode = $responseHeaders['status-code'] ?? 0;
if ($statusCode >= 400) {
throw new Exception("Failed to get merge request files: HTTP {$statusCode}");
}

$files = $response['body'] ?? [];
if (!is_array($files) || empty($files)) {
break;
}

foreach ($files as $diff) {
$allFiles[] = [
'filename' => $diff['new_path'] ?? $diff['old_path'] ?? '',
];
}

if (count($files) < $perPage) {
break;
}
$page++;
}

return $allFiles;
}

public function getPullRequestFromBranch(string $owner, string $repositoryName, string $branch): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$url = "/projects/{$projectPath}/merge_requests?state=opened&source_branch=" . urlencode($branch);

$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$statusCode = $responseHeaders['status-code'] ?? 0;
if ($statusCode >= 400) {
throw new Exception("Failed to list merge requests: HTTP {$statusCode}");
}

$body = $response['body'] ?? [];
if (empty($body[0])) {
return [];
}

$mr = $body[0];

return [
'number' => $mr['iid'] ?? 0,
'title' => $mr['title'] ?? '',
'state' => $mr['state'] ?? '',
'head' => [
'ref' => $mr['source_branch'] ?? '',
'sha' => $mr['sha'] ?? '',
],
'base' => [
'ref' => $mr['target_branch'] ?? '',
],
];
}

public function listBranches(string $owner, string $repositoryName): array
Expand Down Expand Up @@ -704,14 +905,24 @@ public function generateCloneCommand(string $owner, string $repositoryName, stri
public function getEvent(string $event, string $payload): array
{
$payloadArray = json_decode($payload, true);
if ($payloadArray === false || $payloadArray === null) {
if ($payloadArray === null || !is_array($payloadArray)) {
return [];
}

switch ($event) {
case 'Push Hook':
$commits = $payloadArray['commits'] ?? [];
$latestCommit = !empty($commits) ? $commits[0] : [];
$checkoutSha = $payloadArray['checkout_sha'] ?? '';
$latestCommit = [];
foreach ($commits as $c) {
if (($c['id'] ?? '') === $checkoutSha) {
$latestCommit = $c;
break;
}
}
if (empty($latestCommit) && !empty($commits)) {
$latestCommit = $commits[0];
}
$ref = $payloadArray['ref'] ?? '';
// ref format: refs/heads/main
$branch = str_replace('refs/heads/', '', $ref);
Expand Down
Loading