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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Add support for permanent project removal and project restoration
* Add support for `regex` and `sort` in `Repositories::branches`
* Add support for `Users::usersContributedProjects`
* Add support for additional filters and ordering options in `MergeRequests::all`


## [12.0.0] - 2025-02-23
Expand Down
215 changes: 163 additions & 52 deletions src/Api/MergeRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,43 @@ class MergeRequests extends AbstractApi
public const STATE_LOCKED = 'locked';

/**
* @param array $parameters {
* @param array $parameters {
*
* @var int[] $iids return the request having the given iid
* @var string $state return all merge requests or just those that are opened, closed, or
* merged
* @var string $scope Return merge requests for the given scope: created-by-me,
* assigned-to-me or all (default is created-by-me)
* @var string $order_by return requests ordered by created_at or updated_at fields (default is created_at)
* @var string $sort return requests sorted in asc or desc order (default is desc)
* @var string $milestone return merge requests for a specific milestone
* @var string $view if simple, returns the iid, URL, title, description, and basic state of merge request
* @var string $labels return merge requests matching a comma separated list of labels
* @var \DateTimeInterface $created_after return merge requests created after the given time (inclusive)
* @var \DateTimeInterface $created_before return merge requests created before the given time (inclusive)
* @var int $reviewer_id return merge requests which have the user as a reviewer with the given user id
* @var bool $wip return only draft merge requests (true) or only non-draft merge requests (false)
* @var int[] $iids return merge requests having the given IIDs
* @var array|string $approved_by_ids return merge requests approved by the given user IDs
* @var array|string $approved_by_usernames return merge requests approved by the given usernames
* @var array|string $approver_ids return merge requests with the given eligible approver IDs
* @var int|string $assignee_id return merge requests assigned to the given user ID, Any, or None
* @var string[] $assignee_username return merge requests assigned to the given usernames
* @var int $author_id return merge requests created by the given user ID
* @var string $author_username return merge requests created by the given username
* @var \DateTimeInterface $created_after return merge requests created on or after the given time
* @var \DateTimeInterface $created_before return merge requests created on or before the given time
* @var \DateTimeInterface $deployed_after return merge requests deployed after the given time
* @var \DateTimeInterface $deployed_before return merge requests deployed before the given time
* @var string $environment return merge requests deployed to the given environment
* @var string $in change the scope of the search attribute
* @var string $labels return merge requests matching a comma separated list of labels
* @var int $merge_user_id return merge requests merged by the given user ID
* @var string $merge_user_username return merge requests merged by the given username
* @var string $milestone return merge requests for a specific milestone
* @var string $my_reaction_emoji return merge requests reacted to by the authenticated user
* @var array<string,mixed> $not return merge requests that do not match the supplied filters
* @var string $order_by return requests ordered by the given field
* @var int|string $reviewer_id return merge requests reviewed by the given user ID, Any, or None
* @var string $reviewer_username return merge requests reviewed by the given username
* @var string $scope return merge requests for the given scope
* @var string $search search merge requests by title and description
* @var string $sort return requests sorted in asc or desc order
* @var string $source_branch return merge requests with the given source branch
* @var string $state return merge requests with the given state
* @var string $target_branch return merge requests with the given target branch
* @var \DateTimeInterface $updated_after return merge requests updated on or after the given time
* @var \DateTimeInterface $updated_before return merge requests updated on or before the given time
* @var string $view if simple, returns a limited set of merge request fields
* @var bool $with_labels_details include label details in each merge request
* @var bool $with_merge_status_recheck request an asynchronous merge status recalculation
* @var bool $wip return only draft or only non-draft merge requests
* }
*
* @throws UndefinedOptionsException if an option name is undefined
Expand All @@ -75,29 +96,80 @@ public function all(int|string|null $project_id = null, array $parameters = []):

return $utc->format('Y-m-d\TH:i:s.v\Z');
};
$integerArrayValidator = function (array $value): bool {
return \count($value) === \count(\array_filter($value, 'is_int'));
};
$stringArrayValidator = function (array $value): bool {
return \count($value) === \count(\array_filter($value, 'is_string'));
};
$anyNoneValidator = function ($value): bool {
return \in_array($value, ['Any', 'None'], true);
};
$idOrAnyNoneValidator = function ($value) use ($anyNoneValidator): bool {
return \is_int($value) || $anyNoneValidator($value);
};
$idArrayOrAnyNoneValidator = function ($value) use ($idOrAnyNoneValidator, $anyNoneValidator): bool {
if (\is_string($value)) {
return $anyNoneValidator($value);
}

if (!\is_array($value)) {
return false;
}

return \count($value) === \count(\array_filter($value, $idOrAnyNoneValidator));
};
$stringOrStringArrayValidator = function ($value) use ($stringArrayValidator): bool {
if (\is_string($value)) {
return true;
}

return \is_array($value) && $stringArrayValidator($value);
};
$notFilterValidator = function (array $value): bool {
return [] === \array_diff(\array_keys($value), [
'labels',
'milestone',
'author_id',
'author_username',
'assignee_id',
'assignee_username',
'reviewer_id',
'reviewer_username',
'my_reaction_emoji',
]);
};

$resolver->setDefined('iids')
->setAllowedTypes('iids', 'array')
->setAllowedValues('iids', function (array $value) {
return \count($value) === \count(\array_filter($value, 'is_int'));
})
->setAllowedValues('iids', $integerArrayValidator)
;
$resolver->setDefined('state')
->setAllowedValues('state', [self::STATE_ALL, self::STATE_MERGED, self::STATE_OPENED, self::STATE_CLOSED, self::STATE_LOCKED])
$resolver->setDefined('approved_by_ids')
->setAllowedTypes('approved_by_ids', ['array', 'string'])
->setAllowedValues('approved_by_ids', $idArrayOrAnyNoneValidator)
;
$resolver->setDefined('scope')
->setAllowedValues('scope', ['created-by-me', 'assigned-to-me', 'all'])
$resolver->setDefined('approved_by_usernames')
->setAllowedTypes('approved_by_usernames', ['array', 'string'])
->setAllowedValues('approved_by_usernames', $stringOrStringArrayValidator)
;
$resolver->setDefined('order_by')
->setAllowedValues('order_by', ['created_at', 'updated_at'])
$resolver->setDefined('approver_ids')
->setAllowedTypes('approver_ids', ['array', 'string'])
->setAllowedValues('approver_ids', $idArrayOrAnyNoneValidator)
;
$resolver->setDefined('sort')
->setAllowedValues('sort', ['asc', 'desc'])
$resolver->setDefined('assignee_id')
->setAllowedTypes('assignee_id', ['integer', 'string'])
->setAllowedValues('assignee_id', $idOrAnyNoneValidator)
;
$resolver->setDefined('milestone');
$resolver->setDefined('view')
->setAllowedValues('view', ['simple'])
$resolver->setDefined('assignee_username')
->setAllowedTypes('assignee_username', 'array')
->setAllowedValues('assignee_username', $stringArrayValidator)
;
$resolver->setDefined('author_id')
->setAllowedTypes('author_id', 'integer')
;
$resolver->setDefined('author_username')
->setAllowedTypes('author_username', 'string')
;
$resolver->setDefined('labels');
$resolver->setDefined('created_after')
->setAllowedTypes('created_after', \DateTimeInterface::class)
->setNormalizer('created_after', $datetimeNormalizer)
Expand All @@ -106,7 +178,60 @@ public function all(int|string|null $project_id = null, array $parameters = []):
->setAllowedTypes('created_before', \DateTimeInterface::class)
->setNormalizer('created_before', $datetimeNormalizer)
;

$resolver->setDefined('deployed_after')
->setAllowedTypes('deployed_after', \DateTimeInterface::class)
->setNormalizer('deployed_after', $datetimeNormalizer)
;
$resolver->setDefined('deployed_before')
->setAllowedTypes('deployed_before', \DateTimeInterface::class)
->setNormalizer('deployed_before', $datetimeNormalizer)
;
$resolver->setDefined('environment')
->setAllowedTypes('environment', 'string')
;
$resolver->setDefined('in')
->setAllowedValues('in', ['title', 'description', 'title,description', 'description,title'])
;
$resolver->setDefined('labels');
$resolver->setDefined('merge_user_id')
->setAllowedTypes('merge_user_id', 'integer')
;
$resolver->setDefined('merge_user_username')
->setAllowedTypes('merge_user_username', 'string')
;
$resolver->setDefined('milestone');
$resolver->setDefined('my_reaction_emoji')
->setAllowedTypes('my_reaction_emoji', 'string')
;
$resolver->setDefined('non_archived')
->setAllowedTypes('non_archived', 'bool')
;
$resolver->setDefined('not')
->setAllowedTypes('not', 'array')
->setAllowedValues('not', $notFilterValidator)
;
$resolver->setDefined('order_by')
->setAllowedValues('order_by', ['created_at', 'updated_at', 'merged_at', 'label_priority', 'priority', 'milestone_due', 'popularity', 'title'])
;
$resolver->setDefined('reviewer_id')
->setAllowedTypes('reviewer_id', ['integer', 'string'])
->setAllowedValues('reviewer_id', $idOrAnyNoneValidator)
;
$resolver->setDefined('reviewer_username')
->setAllowedTypes('reviewer_username', 'string')
;
$resolver->setDefined('scope')
->setAllowedValues('scope', ['created_by_me', 'assigned_to_me', 'reviews_for_me', 'all'])
;
$resolver->setDefined('search');
$resolver->setDefined('sort')
->setAllowedValues('sort', ['asc', 'desc'])
;
$resolver->setDefined('source_branch');
$resolver->setDefined('state')
->setAllowedValues('state', [self::STATE_ALL, self::STATE_MERGED, self::STATE_OPENED, self::STATE_CLOSED, self::STATE_LOCKED])
;
$resolver->setDefined('target_branch');
$resolver->setDefined('updated_after')
->setAllowedTypes('updated_after', \DateTimeInterface::class)
->setNormalizer('updated_after', $datetimeNormalizer)
Expand All @@ -115,35 +240,21 @@ public function all(int|string|null $project_id = null, array $parameters = []):
->setAllowedTypes('updated_before', \DateTimeInterface::class)
->setNormalizer('updated_before', $datetimeNormalizer)
;

$resolver->setDefined('scope')
->setAllowedValues('scope', ['created_by_me', 'assigned_to_me', 'all'])
$resolver->setDefined('view')
->setAllowedValues('view', ['simple'])
;
$resolver->setDefined('with_labels_details')
->setAllowedTypes('with_labels_details', 'bool')
;
$resolver->setDefined('author_id')
->setAllowedTypes('author_id', 'integer');

$resolver->setDefined('assignee_id')
->setAllowedTypes('assignee_id', 'integer');

$resolver->setDefined('search');
$resolver->setDefined('source_branch');
$resolver->setDefined('target_branch');
$resolver->setDefined('with_merge_status_recheck')
->setAllowedTypes('with_merge_status_recheck', 'bool')
;
$resolver->setDefined('approved_by_ids')
->setAllowedTypes('approved_by_ids', 'array')
->setAllowedValues('approved_by_ids', function (array $value) {
return \count($value) === \count(\array_filter($value, 'is_int'));
})
;
$resolver->setDefined('reviewer_id')
->setAllowedTypes('reviewer_id', 'integer');
$resolver->setDefined('wip')
->setAllowedTypes('wip', 'boolean')
->addNormalizer('wip', static function ($resolver, $wip) {
return $wip ? 'yes' : 'no';
});
})
;

$path = null === $project_id ? 'merge_requests' : $this->getProjectPath($project_id, 'merge_requests');

Expand Down
Loading