diff --git a/skills/drupalorg-cli/SKILL.md b/skills/drupalorg-cli/SKILL.md index 8e1d629..a8b28c8 100644 --- a/skills/drupalorg-cli/SKILL.md +++ b/skills/drupalorg-cli/SKILL.md @@ -76,26 +76,39 @@ drupalorg issue:link ### Merge Request commands +The first argument can be a Drupal.org issue NID, a `project-path!iid` ref +(e.g. `project/drupal!708`), or a full GitLab MR URL. When using a ref that +includes the MR IID (`!iid`), the second `` argument is not needed. + +> **zsh users:** Escape `!` or quote the argument to prevent history expansion: +> `project/drupal\!708` or `'project/drupal!708'` + ```bash # List merge requests for a Drupal.org issue fork # --state: opened (default), closed, merged, all # nid is optional; auto-detected from the branch name if omitted drupalorg mr:list [nid] [--state=opened] --format=llm +# List MRs by project path (no issue NID needed) +drupalorg mr:list project/drupal --format=llm # Show the unified diff for a merge request # Supports --format=text (default), json drupalorg mr:diff +drupalorg mr:diff 'project/drupal!708' # List changed files in a merge request # Supports --format=text (default), json drupalorg mr:files +drupalorg mr:files 'project/drupal!708' # Show the pipeline status for a merge request drupalorg mr:status --format=llm +drupalorg mr:status 'project/drupal!708' --format=llm # Show failed job traces from the latest pipeline for a merge request # Supports --format=text (default), json drupalorg mr:logs +drupalorg mr:logs 'project/drupal!708' ``` ### Project commands diff --git a/src/Api/Action/MergeRequest/AbstractMergeRequestAction.php b/src/Api/Action/MergeRequest/AbstractMergeRequestAction.php index fb51e8b..4853b23 100644 --- a/src/Api/Action/MergeRequest/AbstractMergeRequestAction.php +++ b/src/Api/Action/MergeRequest/AbstractMergeRequestAction.php @@ -5,6 +5,7 @@ use mglaman\DrupalOrg\Action\ActionInterface; use mglaman\DrupalOrg\Client; use mglaman\DrupalOrg\GitLab\Client as GitLabClient; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; abstract class AbstractMergeRequestAction implements ActionInterface { @@ -24,4 +25,13 @@ protected function resolveGitLabProject(string $nid): array $project = $this->gitLabClient->getProject($projectPath); return [(int) $project->id, $projectPath]; } + + /** + * @return array{0: int, 1: string} + */ + protected function resolveFromRef(MergeRequestRef $ref): array + { + $project = $this->gitLabClient->getProject($ref->projectPath); + return [(int) $project->id, $ref->projectPath]; + } } diff --git a/src/Api/Action/MergeRequest/GetMergeRequestDiffAction.php b/src/Api/Action/MergeRequest/GetMergeRequestDiffAction.php index 3e308b1..0cb91b7 100644 --- a/src/Api/Action/MergeRequest/GetMergeRequestDiffAction.php +++ b/src/Api/Action/MergeRequest/GetMergeRequestDiffAction.php @@ -2,13 +2,14 @@ namespace mglaman\DrupalOrg\Action\MergeRequest; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; use mglaman\DrupalOrg\Result\MergeRequest\MergeRequestDiffResult; class GetMergeRequestDiffAction extends AbstractMergeRequestAction { - public function __invoke(string $nid, int $mrIid): MergeRequestDiffResult + public function __invoke(string $nid, int $mrIid, ?MergeRequestRef $ref = null): MergeRequestDiffResult { - [$projectId] = $this->resolveGitLabProject($nid); + [$projectId] = $ref !== null ? $this->resolveFromRef($ref) : $this->resolveGitLabProject($nid); $mr = $this->gitLabClient->getMergeRequest($projectId, $mrIid); $diffs = $this->gitLabClient->getMergeRequestDiffs($projectId, $mrIid); diff --git a/src/Api/Action/MergeRequest/GetMergeRequestFilesAction.php b/src/Api/Action/MergeRequest/GetMergeRequestFilesAction.php index 7704d17..0f457df 100644 --- a/src/Api/Action/MergeRequest/GetMergeRequestFilesAction.php +++ b/src/Api/Action/MergeRequest/GetMergeRequestFilesAction.php @@ -2,13 +2,14 @@ namespace mglaman\DrupalOrg\Action\MergeRequest; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; use mglaman\DrupalOrg\Result\MergeRequest\MergeRequestFilesResult; class GetMergeRequestFilesAction extends AbstractMergeRequestAction { - public function __invoke(string $nid, int $mrIid): MergeRequestFilesResult + public function __invoke(string $nid, int $mrIid, ?MergeRequestRef $ref = null): MergeRequestFilesResult { - [$projectId] = $this->resolveGitLabProject($nid); + [$projectId] = $ref !== null ? $this->resolveFromRef($ref) : $this->resolveGitLabProject($nid); $diffs = $this->gitLabClient->getMergeRequestDiffs($projectId, $mrIid); diff --git a/src/Api/Action/MergeRequest/GetMergeRequestLogsAction.php b/src/Api/Action/MergeRequest/GetMergeRequestLogsAction.php index 35d3730..c292217 100644 --- a/src/Api/Action/MergeRequest/GetMergeRequestLogsAction.php +++ b/src/Api/Action/MergeRequest/GetMergeRequestLogsAction.php @@ -2,15 +2,16 @@ namespace mglaman\DrupalOrg\Action\MergeRequest; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; use mglaman\DrupalOrg\Result\MergeRequest\MergeRequestLogsResult; class GetMergeRequestLogsAction extends AbstractMergeRequestAction { private const TRACE_EXCERPT_LINES = 100; - public function __invoke(string $nid, int $mrIid): MergeRequestLogsResult + public function __invoke(string $nid, int $mrIid, ?MergeRequestRef $ref = null): MergeRequestLogsResult { - [$projectId] = $this->resolveGitLabProject($nid); + [$projectId] = $ref !== null ? $this->resolveFromRef($ref) : $this->resolveGitLabProject($nid); $pipelines = $this->gitLabClient->getMergeRequestPipelines($projectId, $mrIid); diff --git a/src/Api/Action/MergeRequest/GetMergeRequestStatusAction.php b/src/Api/Action/MergeRequest/GetMergeRequestStatusAction.php index be1d98a..592d8e7 100644 --- a/src/Api/Action/MergeRequest/GetMergeRequestStatusAction.php +++ b/src/Api/Action/MergeRequest/GetMergeRequestStatusAction.php @@ -2,13 +2,14 @@ namespace mglaman\DrupalOrg\Action\MergeRequest; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; use mglaman\DrupalOrg\Result\MergeRequest\MergeRequestStatusResult; class GetMergeRequestStatusAction extends AbstractMergeRequestAction { - public function __invoke(string $nid, int $mrIid): MergeRequestStatusResult + public function __invoke(string $nid, int $mrIid, ?MergeRequestRef $ref = null): MergeRequestStatusResult { - [$projectId] = $this->resolveGitLabProject($nid); + [$projectId] = $ref !== null ? $this->resolveFromRef($ref) : $this->resolveGitLabProject($nid); $pipelines = $this->gitLabClient->getMergeRequestPipelines($projectId, $mrIid); diff --git a/src/Api/Action/MergeRequest/ListMergeRequestsAction.php b/src/Api/Action/MergeRequest/ListMergeRequestsAction.php index f32e991..c900963 100644 --- a/src/Api/Action/MergeRequest/ListMergeRequestsAction.php +++ b/src/Api/Action/MergeRequest/ListMergeRequestsAction.php @@ -3,14 +3,15 @@ namespace mglaman\DrupalOrg\Action\MergeRequest; use mglaman\DrupalOrg\Enum\MergeRequestState; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; use mglaman\DrupalOrg\Result\MergeRequest\MergeRequestItem; use mglaman\DrupalOrg\Result\MergeRequest\MergeRequestListResult; class ListMergeRequestsAction extends AbstractMergeRequestAction { - public function __invoke(string $nid, MergeRequestState $state = MergeRequestState::Opened): MergeRequestListResult + public function __invoke(string $nid, MergeRequestState $state = MergeRequestState::Opened, ?MergeRequestRef $ref = null): MergeRequestListResult { - [$projectId, $gitLabProjectPath] = $this->resolveGitLabProject($nid); + [$projectId, $gitLabProjectPath] = $ref !== null ? $this->resolveFromRef($ref) : $this->resolveGitLabProject($nid); $params = ['per_page' => 100]; if ($state !== MergeRequestState::All) { diff --git a/src/Api/GitLab/MergeRequestRef.php b/src/Api/GitLab/MergeRequestRef.php new file mode 100644 index 0000000..0dc5343 --- /dev/null +++ b/src/Api/GitLab/MergeRequestRef.php @@ -0,0 +1,77 @@ +stdIn->getOption('format') ?? 'text'); $action = new GetMergeRequestDiffAction($this->client, new GitLabClient()); - $result = $action($this->nid, $this->mrIid); + $result = $action($this->nid ?? '', $this->mrIid, $this->mrRef); if ($format === 'json') { $this->stdOut->writeln((string) json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); diff --git a/src/Cli/Command/MergeRequest/GetFiles.php b/src/Cli/Command/MergeRequest/GetFiles.php index daa0c8f..6f73e1f 100644 --- a/src/Cli/Command/MergeRequest/GetFiles.php +++ b/src/Cli/Command/MergeRequest/GetFiles.php @@ -31,7 +31,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $format = (string) ($this->stdIn->getOption('format') ?? 'text'); $action = new GetMergeRequestFilesAction($this->client, new GitLabClient()); - $result = $action($this->nid, $this->mrIid); + $result = $action($this->nid ?? '', $this->mrIid, $this->mrRef); if ($format === 'json') { $this->stdOut->writeln((string) json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); diff --git a/src/Cli/Command/MergeRequest/GetLogs.php b/src/Cli/Command/MergeRequest/GetLogs.php index ebf14ea..8080d83 100644 --- a/src/Cli/Command/MergeRequest/GetLogs.php +++ b/src/Cli/Command/MergeRequest/GetLogs.php @@ -30,7 +30,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $format = (string) ($this->stdIn->getOption('format') ?? 'text'); $action = new GetMergeRequestLogsAction($this->client, new GitLabClient()); - $result = $action($this->nid, $this->mrIid); + $result = $action($this->nid ?? '', $this->mrIid, $this->mrRef); if ($format === 'json') { $this->stdOut->writeln((string) json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); diff --git a/src/Cli/Command/MergeRequest/GetStatus.php b/src/Cli/Command/MergeRequest/GetStatus.php index c161c6c..1294db3 100644 --- a/src/Cli/Command/MergeRequest/GetStatus.php +++ b/src/Cli/Command/MergeRequest/GetStatus.php @@ -30,7 +30,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $format = (string) ($this->stdIn->getOption('format') ?? 'text'); $action = new GetMergeRequestStatusAction($this->client, new GitLabClient()); - $result = $action($this->nid, $this->mrIid); + $result = $action($this->nid ?? '', $this->mrIid, $this->mrRef); if ($this->writeFormatted($result, $format)) { return 0; diff --git a/src/Cli/Command/MergeRequest/ListMergeRequests.php b/src/Cli/Command/MergeRequest/ListMergeRequests.php index decc5be..193acc8 100644 --- a/src/Cli/Command/MergeRequest/ListMergeRequests.php +++ b/src/Cli/Command/MergeRequest/ListMergeRequests.php @@ -5,6 +5,8 @@ use mglaman\DrupalOrg\Action\MergeRequest\ListMergeRequestsAction; use mglaman\DrupalOrg\Enum\MergeRequestState; use mglaman\DrupalOrg\GitLab\Client as GitLabClient; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; +use mglaman\DrupalOrgCli\Command\Command; use mglaman\DrupalOrgCli\Command\Issue\IssueCommandBase; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; @@ -16,12 +18,14 @@ class ListMergeRequests extends IssueCommandBase { protected bool $requiresRepository = false; + protected ?MergeRequestRef $mrRef = null; + protected function configure(): void { $this ->setName('mr:list') ->setAliases(['mrl']) - ->addArgument('nid', InputArgument::OPTIONAL, 'The issue node ID') + ->addArgument('nid', InputArgument::OPTIONAL, 'The issue NID, project-path (or project-path!iid; quote in zsh), or GitLab URL') ->addOption( 'state', null, @@ -36,7 +40,22 @@ protected function configure(): void 'Output format: text, json, md, llm. Defaults to text.', 'text' ) - ->setDescription('List merge requests for a Drupal.org issue fork.'); + ->setDescription('List merge requests for a Drupal.org issue fork or project.'); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $nidArg = (string) $input->getArgument('nid'); + $ref = $nidArg !== '' ? MergeRequestRef::tryParse($nidArg) : null; + + if ($ref !== null) { + Command::initialize($input, $output); + $this->mrRef = $ref; + $this->nid = ''; + return; + } + + parent::initialize($input, $output); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -45,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $format = (string) ($this->stdIn->getOption('format') ?? 'text'); $action = new ListMergeRequestsAction($this->client, new GitLabClient()); - $result = $action($this->nid, $state); + $result = $action($this->nid ?? '', $state, $this->mrRef); if ($this->writeFormatted($result, $format)) { return 0; diff --git a/src/Cli/Command/MergeRequest/MrCommandBase.php b/src/Cli/Command/MergeRequest/MrCommandBase.php index b091afd..63ff407 100644 --- a/src/Cli/Command/MergeRequest/MrCommandBase.php +++ b/src/Cli/Command/MergeRequest/MrCommandBase.php @@ -2,6 +2,8 @@ namespace mglaman\DrupalOrgCli\Command\MergeRequest; +use mglaman\DrupalOrg\GitLab\MergeRequestRef; +use mglaman\DrupalOrgCli\Command\Command; use mglaman\DrupalOrgCli\Command\Issue\IssueCommandBase; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -13,15 +15,38 @@ abstract class MrCommandBase extends IssueCommandBase protected int $mrIid; + protected ?MergeRequestRef $mrRef = null; + protected function configureNidAndMrIid(): void { $this - ->addArgument('nid', InputArgument::OPTIONAL, 'The issue node ID') + ->addArgument('nid', InputArgument::OPTIONAL, 'The issue NID, project-path!iid (quote in zsh), or GitLab MR URL') ->addArgument('mr-iid', InputArgument::OPTIONAL, 'The merge request IID'); } protected function initialize(InputInterface $input, OutputInterface $output): void { + $nidArg = (string) $input->getArgument('nid'); + $ref = $nidArg !== '' ? MergeRequestRef::tryParse($nidArg) : null; + + if ($ref !== null) { + // Skip IssueCommandBase NID resolution — go straight to Command::initialize. + Command::initialize($input, $output); + $this->mrRef = $ref; + $this->nid = ''; + + if ($ref->mrIid !== null) { + $this->mrIid = $ref->mrIid; + } else { + $mrIidArg = $input->getArgument('mr-iid'); + if ($mrIidArg === null || $mrIidArg === '') { + throw new \RuntimeException('Argument mr-iid is required when project-path has no !iid.'); + } + $this->mrIid = (int) $mrIidArg; + } + return; + } + parent::initialize($input, $output); $mrIid = $this->stdIn->getArgument('mr-iid'); diff --git a/tests/src/GitLab/MergeRequestRefTest.php b/tests/src/GitLab/MergeRequestRefTest.php new file mode 100644 index 0000000..4a4c589 --- /dev/null +++ b/tests/src/GitLab/MergeRequestRefTest.php @@ -0,0 +1,83 @@ +projectPath); + self::assertSame($expectedIid, $ref->mrIid); + } + + /** + * @return array + */ + public static function parseProvider(): array + { + return [ + 'project-path with IID' => [ + 'project/canvas!708', + 'project/canvas', + 708, + ], + 'project-path without IID' => [ + 'project/canvas', + 'project/canvas', + null, + ], + 'full URL with MR IID' => [ + 'https://git.drupalcode.org/project/canvas/-/merge_requests/708', + 'project/canvas', + 708, + ], + 'full URL without MR' => [ + 'https://git.drupalcode.org/project/canvas', + 'project/canvas', + null, + ], + 'full URL with trailing slash' => [ + 'https://git.drupalcode.org/project/drupal/', + 'project/drupal', + null, + ], + 'numeric NID falls through' => [ + '1234567', + null, + null, + ], + 'random string falls through' => [ + 'hello-world', + null, + null, + ], + 'empty string falls through' => [ + '', + null, + null, + ], + 'project with hyphens and underscores' => [ + 'project/my_cool-module!42', + 'project/my_cool-module', + 42, + ], + ]; + } +}