Skip to content
Merged
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
6 changes: 5 additions & 1 deletion skill-data/drupalorg-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,13 @@ with clearly labelled fields, contributor lists, and change records.
drupalorg issue:show <nid> --format=llm

# Fetch issue details including all comments (skips system-generated messages)
# Note: --with-comments only applies to D.o issues
# Works for both D.o issues and GitLab work items
drupalorg issue:show <nid> --with-comments --format=llm

# GitLab work items only: also keep drupalbot replies to slash commands.
# Off by default because the work item's labels and assignees already reflect them.
drupalorg issue:show <ref> --with-comments --include-bot-comments --format=llm

# Show the GitLab issue fork URLs and branches
# nid is optional; auto-detected from the branch name if omitted
drupalorg issue:get-fork [nid] --format=llm
Expand Down
7 changes: 0 additions & 7 deletions skill-data/drupalorg-cli/references/ai-contribution-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ drupalorg issue:show <nid> --with-comments --format=llm
drupalorg mr:list <nid> --state=all --format=llm
```

`--with-comments` only returns comments for classic Drupal.org issues. For GitLab
work items, read the discussion with `glab` or ask the user to summarize it:

```bash
GITLAB_HOST=git.drupalcode.org glab issue view <nid> --comments --repo project/<name>
```

From that output, report to the user before proposing a change:

- Previous attempts (patches, MRs, closed MRs) and why they stalled.
Expand Down
7 changes: 0 additions & 7 deletions skill-data/drupalorg-work-on-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,6 @@ into an issue that ignores the discussion or reopens settled decisions is a poli
violation. If your reading of the code disagrees with the thread's direction, say so
to the user and let them raise it in the issue. Do not act on it unilaterally.

`--with-comments` is ignored for GitLab work items. For those, read the discussion
with `glab` or ask the user to summarize it:

```bash
GITLAB_HOST=git.drupalcode.org glab issue view <nid> --comments --repo project/<name>
```

**Directory detection:** Before prompting the user, read `CLAUDE.md` in the current directory.
If it documents the path to the `<project>` module or repository, `cd` there automatically
and skip the directory prompt. Only fall back to running `git remote get-url origin` and
Expand Down
26 changes: 24 additions & 2 deletions src/Api/Action/GitLab/GetGitLabIssueAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,40 @@

use mglaman\DrupalOrg\GitLab\Client as GitLabClient;
use mglaman\DrupalOrg\GitLab\Entity\GitLabIssue;
use mglaman\DrupalOrg\GitLab\Entity\GitLabNote;
use mglaman\DrupalOrg\GitLab\WorkItemRef;
use mglaman\DrupalOrg\Result\GitLab\GitLabIssueResult;

class GetGitLabIssueAction
{
/**
* The Drupal.org bot that answers slash commands on work items.
*/
public const BOT_USERNAME = 'drupalbot';

public function __construct(private readonly GitLabClient $gitLabClient)
{
}

public function __invoke(WorkItemRef $ref): GitLabIssueResult
public function __invoke(WorkItemRef $ref, bool $withComments = false, bool $includeBotComments = false): GitLabIssueResult
{
$data = $this->gitLabClient->getIssue($ref->projectPath, $ref->issueId);
return new GitLabIssueResult(GitLabIssue::fromStdClass($data));
$issue = GitLabIssue::fromStdClass($data);
if (!$withComments) {
return new GitLabIssueResult($issue);
}

$comments = [];
foreach ($this->gitLabClient->getIssueNotes($ref->projectPath, $ref->issueId) as $noteData) {
$note = GitLabNote::fromStdClass($noteData);
if ($note->system) {
continue;
}
if (!$includeBotComments && $note->author === self::BOT_USERNAME) {
continue;
}
$comments[] = $note;
}
return new GitLabIssueResult($issue, $comments);
}
}
23 changes: 23 additions & 0 deletions src/Api/GitLab/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ public function postIssueNote(string $projectPath, int $iid, string $body): \std
return $result;
}

/**
* GET /projects/{path}/issues/{iid}/notes
*
* Returns every note on a GitLab issue or work item in chronological order,
* following pagination. Callers decide whether to keep system notes.
*
* @return \stdClass[]
* @throws \Exception
*/
public function getIssueNotes(string $projectPath, int $iid): array
{
$path = 'projects/' . urlencode($projectPath) . '/issues/' . $iid . '/notes';
$notes = [];
$page = 1;
do {
$result = $this->get($path, ['per_page' => 100, 'page' => $page, 'sort' => 'asc', 'order_by' => 'created_at']);
$batch = is_array($result) ? $result : [];
$notes = [...$notes, ...$batch];
$page++;
} while (count($batch) === 100);
return $notes;
}

/**
* GET /projects/{path}
*
Expand Down
38 changes: 38 additions & 0 deletions src/Api/GitLab/Entity/GitLabNote.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace mglaman\DrupalOrg\GitLab\Entity;

class GitLabNote implements \JsonSerializable
{
public function __construct(
public readonly int $id,
public readonly string $body,
public readonly string $author,
public readonly string $createdAt,
public readonly bool $system,
) {
}

public static function fromStdClass(\stdClass $data): self
{
return new self(
id: (int) ($data->id ?? 0),
body: (string) ($data->body ?? ''),
author: (string) ($data->author->username ?? $data->author->name ?? ''),
createdAt: (string) ($data->created_at ?? ''),
system: (bool) ($data->system ?? false),
);
}

public function jsonSerialize(): mixed
{
return [
'id' => $this->id,
'body' => $this->body,
'author' => $this->author,
'created_at' => $this->createdAt,
];
}
}
6 changes: 6 additions & 0 deletions src/Api/Result/GitLab/GitLabIssueResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
namespace mglaman\DrupalOrg\Result\GitLab;

use mglaman\DrupalOrg\GitLab\Entity\GitLabIssue;
use mglaman\DrupalOrg\GitLab\Entity\GitLabNote;
use mglaman\DrupalOrg\Result\ResultInterface;

class GitLabIssueResult implements ResultInterface
{
/**
* @param GitLabNote[] $comments
*/
public function __construct(
public readonly GitLabIssue $issue,
public readonly array $comments = [],
) {
}

Expand All @@ -27,6 +32,7 @@ public function jsonSerialize(): mixed
'web_url' => $this->issue->webUrl,
'author' => $this->issue->author,
'assignees' => $this->issue->assignees,
'comments' => array_map(static fn(GitLabNote $note) => $note->jsonSerialize(), $this->comments),
];
}
}
2 changes: 1 addition & 1 deletion src/Cli/Command/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ protected function writeFormatted(ResultInterface $result, string $format): bool
'llm' => new LlmFormatter(),
default => throw new \InvalidArgumentException("Unknown format: $format"),
};
$this->stdOut->writeln($formatter->format($result));
$this->stdOut->writeln($formatter->format($result), OutputInterface::OUTPUT_RAW);
return true;
}

Expand Down
17 changes: 14 additions & 3 deletions src/Cli/Command/Issue/Show.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ protected function configure(): void
'Output options: text, json, md, llm. Defaults to text.',
'text'
)
->addOption('with-comments', null, InputOption::VALUE_NONE, 'Also fetch issue comments.')
->addOption('with-comments', null, InputOption::VALUE_NONE, 'Also fetch issue comments. System-generated messages are skipped.')
->addOption('include-bot-comments', null, InputOption::VALUE_NONE, 'Keep drupalbot replies when fetching GitLab work item comments. Off by default because the work item fields already reflect label and assignee changes.')
->setDescription('Show a given issue information.');
}

Expand All @@ -38,9 +39,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$nid = $this->stdIn->getArgument('nid');
$format = $this->stdIn->getOption('format');

$withComments = (bool) $this->stdIn->getOption('with-comments');
$ref = WorkItemRef::tryParse((string) $nid);
if ($ref !== null) {
$result = (new GetGitLabIssueAction(new GitLabClient()))($ref);
$includeBotComments = (bool) $this->stdIn->getOption('include-bot-comments');
$result = (new GetGitLabIssueAction(new GitLabClient()))($ref, $withComments, $includeBotComments);
if ($this->writeFormatted($result, (string) $format)) {
return 0;
}
Expand All @@ -58,10 +61,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->stdOut->writeln(sprintf('Updated: %s', $issue->updatedAt));
$this->stdOut->writeln(sprintf('URL: %s', $issue->webUrl));
$this->stdOut->writeln(sprintf("\nDescription:\n%s", $issue->description));
foreach ($result->comments as $index => $comment) {
$this->stdOut->writeln(sprintf(
"\nComment #%d by %s (%s):\n%s",
$index + 1,
$comment->author,
$comment->createdAt,
$comment->body
));
}
return 0;
}

$withComments = (bool) $this->stdIn->getOption('with-comments');
$result = (new GetIssueAction($this->client))($nid, $withComments);

if ($this->writeFormatted($result, (string) $format)) {
Expand Down
20 changes: 19 additions & 1 deletion src/Cli/Formatter/LlmFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,24 @@ protected function formatGitLabIssue(GitLabIssueResult $result): string
$assigneesXml .= ' <assignee>' . $this->xmlEscape($assignee) . "</assignee>\n";
}

$commentsXml = '';
if ($result->comments !== []) {
$commentsXml = "\n <comments>";
foreach ($result->comments as $index => $comment) {
$number = $index + 1;
$commentAuthor = $this->xmlEscape($comment->author);
$commentCreated = $this->xmlEscape($comment->createdAt);
$body = $this->cdataWrap($comment->body);
$commentsXml .= "\n <comment>";
$commentsXml .= "\n <number>{$number}</number>";
$commentsXml .= "\n <author>{$commentAuthor}</author>";
$commentsXml .= "\n <created>{$commentCreated}</created>";
$commentsXml .= "\n <body>{$body}</body>";
$commentsXml .= "\n </comment>";
}
$commentsXml .= "\n </comments>";
}

return <<<XML
<gitlab_context>
<issue_id>{$issue->iid}</issue_id>
Expand All @@ -287,7 +305,7 @@ protected function formatGitLabIssue(GitLabIssueResult $result): string
{$labelsXml} </labels>
<assignees>
{$assigneesXml} </assignees>
<description>{$description}</description>
<description>{$description}</description>{$commentsXml}
</gitlab_context>
XML;
}
Expand Down
10 changes: 10 additions & 0 deletions src/Cli/Formatter/MarkdownFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ protected function formatGitLabIssue(GitLabIssueResult $result): string
$lines[] = '## Description';
$lines[] = '';
$lines[] = $issue->description;
if ($result->comments !== []) {
$lines[] = '';
$lines[] = '## Comments';
foreach ($result->comments as $index => $comment) {
$lines[] = '';
$lines[] = sprintf('### Comment #%d — %s (%s)', $index + 1, $comment->author, $comment->createdAt);
$lines[] = '';
$lines[] = $comment->body;
}
}
return implode("\n", $lines);
}

Expand Down
Loading