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 @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Add support for personal access tokens
* Add support for project integrations endpoints
* Add support for `Projects::updateDeployKey`
* Add support for project push rules
* Add support for group hook endpoints
* Add support for `job_inputs` and `job_variables_attributes` in `Jobs::play`
* Add support for `inputs` in `Projects::createPipeline`
Expand Down
99 changes: 99 additions & 0 deletions src/Api/Projects.php
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,68 @@ public function deleteDeployToken(int|string $project_id, int $token_id): mixed
return $this->delete($this->getProjectPath($project_id, 'deploy_tokens/'.self::encodePath($token_id)));
}

public function pushRule(int|string $project_id): mixed
{
return $this->get($this->getProjectPath($project_id, 'push_rule'));
}

/**
* @param array $parameters {
*
* @var string $author_email_regex all commit author emails must match this regular expression
* @var string $branch_name_regex all branch names must match this regular expression
* @var bool $commit_committer_check only allow commits when the committer email is one of the user's verified emails
* @var bool $commit_committer_name_check only allow commits when the author name matches the user's GitLab account name
* @var string $commit_message_negative_regex reject commit messages matching this regular expression
* @var string $commit_message_regex require commit messages to match this regular expression
* @var bool $deny_delete_tag deny deleting tags
* @var string $file_name_regex reject committed filenames matching this regular expression
* @var int $max_file_size maximum file size in MB
* @var bool $member_check restrict commit authors by email to existing GitLab users
* @var bool $prevent_secrets reject files likely to contain secrets
* @var bool $reject_non_dco_commits reject commits that are not DCO certified
* @var bool $reject_unsigned_commits reject unsigned commits
* }
*
* @throws UndefinedOptionsException If an option name is undefined
* @throws InvalidOptionsException If an option doesn't fulfill the specified validation rules
*/
public function createPushRule(int|string $project_id, array $parameters = []): mixed
{
return $this->post($this->getProjectPath($project_id, 'push_rule'), self::createPushRuleOptionsResolver()->resolve($parameters));
}

/**
* @param array $parameters {
*
* @var string $author_email_regex all commit author emails must match this regular expression
* @var string $branch_name_regex all branch names must match this regular expression
* @var bool $commit_committer_check only allow commits when the committer email is one of the user's verified emails
* @var bool $commit_committer_name_check only allow commits when the author name matches the user's GitLab account name
* @var string $commit_message_negative_regex reject commit messages matching this regular expression
* @var string $commit_message_regex require commit messages to match this regular expression
* @var bool $deny_delete_tag deny deleting tags
* @var string $file_name_regex reject committed filenames matching this regular expression
* @var int $max_file_size maximum file size in MB
* @var bool $member_check restrict commit authors by email to existing GitLab users
* @var bool $prevent_secrets reject files likely to contain secrets
* @var bool $reject_non_dco_commits reject commits that are not DCO certified
* @var bool $reject_unsigned_commits reject unsigned commits
* }
*
* @throws UndefinedOptionsException If an option name is undefined
* @throws InvalidOptionsException If an option doesn't fulfill the specified validation rules
*/
public function updatePushRule(int|string $project_id, array $parameters = []): mixed
{
return $this->put($this->getProjectPath($project_id, 'push_rule'), self::createPushRuleOptionsResolver()->resolve($parameters));
}

public function deletePushRule(int|string $project_id): mixed
{
return $this->delete($this->getProjectPath($project_id, 'push_rule'));
}

/**
* @param array $parameters {
*
Expand Down Expand Up @@ -1503,4 +1565,41 @@ public function search(int|string $id, array $parameters = []): mixed

return $this->get('projects/'.self::encodePath($id).'/search', $resolver->resolve($parameters));
}

private static function createPushRuleOptionsResolver(): OptionsResolver
{
$resolver = new OptionsResolver();

foreach ([
'author_email_regex',
'branch_name_regex',
'commit_message_negative_regex',
'commit_message_regex',
'file_name_regex',
] as $option) {
$resolver->setDefined($option)
->setAllowedTypes($option, 'string')
;
}

foreach ([
'commit_committer_check',
'commit_committer_name_check',
'deny_delete_tag',
'member_check',
'prevent_secrets',
'reject_non_dco_commits',
'reject_unsigned_commits',
] as $option) {
$resolver->setDefined($option)
->setAllowedTypes($option, 'bool')
;
}

$resolver->setDefined('max_file_size')
->setAllowedTypes('max_file_size', 'int')
;

return $resolver;
}
}
182 changes: 182 additions & 0 deletions tests/Api/ProjectsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;

class ProjectsTest extends TestCase
{
Expand Down Expand Up @@ -1600,6 +1602,186 @@ public function shouldDeleteDeployToken(): void
$this->assertEquals($expectedBool, $api->deleteDeployToken(1, 2));
}

#[Test]
public function shouldGetPushRule(): void
{
$expectedArray = [
'id' => 1,
'project_id' => 3,
'commit_message_regex' => 'Fixes \\d+\\..*',
'branch_name_regex' => 'feature\\/.*',
'author_email_regex' => '@example.com$',
];

$api = $this->getApiMock();
$api->expects($this->once())
->method('get')
->with('projects/3/push_rule')
->willReturn($expectedArray);

$this->assertEquals($expectedArray, $api->pushRule(3));
}

#[Test]
public function shouldGetUnsetPushRule(): void
{
$api = $this->getApiMock();
$api->expects($this->once())
->method('get')
->with('projects/3/push_rule')
->willReturn('null');

$this->assertEquals('null', $api->pushRule(3));
}

#[Test]
public function shouldCreatePushRule(): void
{
$expectedBool = true;
$parameters = [
'commit_message_regex' => 'Fixes \\d+\\..*',
'branch_name_regex' => 'feature\\/.*',
'author_email_regex' => '@example.com$',
];

$api = $this->getApiMock();
$api->expects($this->once())
->method('post')
->with('projects/3/push_rule', $parameters)
->willReturn($expectedBool);

$this->assertEquals($expectedBool, $api->createPushRule(3, $parameters));
}

#[Test]
public function shouldCreatePushRuleWithBooleanAndIntegerParameters(): void
{
$expectedBool = true;
$parameters = [
'deny_delete_tag' => false,
'member_check' => true,
'prevent_secrets' => true,
'commit_committer_check' => false,
'commit_committer_name_check' => true,
'reject_unsigned_commits' => false,
'reject_non_dco_commits' => true,
'max_file_size' => 100,
];

$api = $this->getApiMock();
$api->expects($this->once())
->method('post')
->with('projects/3/push_rule', $parameters)
->willReturn($expectedBool);

$this->assertEquals($expectedBool, $api->createPushRule(3, $parameters));
}

#[Test]
public function shouldUpdatePushRule(): void
{
$expectedBool = true;
$parameters = [
'commit_message_regex' => 'Fixes \\d+\\..*',
'branch_name_regex' => 'feature\\/.*',
'author_email_regex' => '@example.com$',
];

$api = $this->getApiMock();
$api->expects($this->once())
->method('put')
->with('projects/3/push_rule', $parameters)
->willReturn($expectedBool);

$this->assertEquals($expectedBool, $api->updatePushRule(3, $parameters));
}

#[Test]
public function shouldUpdatePushRuleWithBooleanAndIntegerParameters(): void
{
$expectedBool = true;
$parameters = [
'deny_delete_tag' => true,
'member_check' => false,
'prevent_secrets' => false,
'commit_committer_check' => true,
'commit_committer_name_check' => false,
'reject_unsigned_commits' => true,
'reject_non_dco_commits' => false,
'max_file_size' => 25,
];

$api = $this->getApiMock();
$api->expects($this->once())
->method('put')
->with('projects/3/push_rule', $parameters)
->willReturn($expectedBool);

$this->assertEquals($expectedBool, $api->updatePushRule(3, $parameters));
}

#[Test]
public function shouldDeletePushRule(): void
{
$expectedBool = true;

$api = $this->getApiMock();
$api->expects($this->once())
->method('delete')
->with('projects/3/push_rule')
->willReturn($expectedBool);

$this->assertEquals($expectedBool, $api->deletePushRule(3));
}

#[Test]
public function shouldRejectUndefinedParameterWhenCreatingPushRule(): void
{
$api = $this->getApiMock();
$api->expects($this->never())
->method('post');

$this->expectException(UndefinedOptionsException::class);

$api->createPushRule(3, ['unsupported_parameter' => true]);
}

#[Test]
public function shouldRejectUndefinedParameterWhenUpdatingPushRule(): void
{
$api = $this->getApiMock();
$api->expects($this->never())
->method('put');

$this->expectException(UndefinedOptionsException::class);

$api->updatePushRule(3, ['unsupported_parameter' => true]);
}

#[Test]
public function shouldRequireIntegerMaxFileSizeWhenCreatingPushRule(): void
{
$api = $this->getApiMock();
$api->expects($this->never())
->method('post');

$this->expectException(InvalidOptionsException::class);

$api->createPushRule(3, ['max_file_size' => '100']);
}

#[Test]
public function shouldRequireIntegerMaxFileSizeWhenUpdatingPushRule(): void
{
$api = $this->getApiMock();
$api->expects($this->never())
->method('put');

$this->expectException(InvalidOptionsException::class);

$api->updatePushRule(3, ['max_file_size' => '100']);
}

#[Test]
public function shouldGetEvents(): void
{
Expand Down