Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate Git branch naming convention #328 #329

Merged
merged 11 commits into from
Mar 16, 2017
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ parameters:
doctrine_orm: ~
gherkin: ~
git_blacklist: ~
git_branch_name: ~
git_commit_message: ~
git_conflict: ~
grunt: ~
Expand Down
2 changes: 2 additions & 0 deletions doc/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ parameters:
doctrine_orm: ~
gherkin: ~
git_blacklist: ~
git_branch_name: ~
git_commit_message: ~
git_conflict: ~
grunt: ~
Expand Down Expand Up @@ -60,6 +61,7 @@ Every task has it's own default configuration. It is possible to overwrite the p
- [Doctrine ORM](tasks/doctrine_orm.md)
- [Gherkin](tasks/gherkin.md)
- [Git blacklist](tasks/git_blacklist.md)
- [Git branch name](tasks/git_branch_name.md)
- [Git commit message](tasks/git_commit_message.md)
- [Git conflict](tasks/git_conflict.md)
- [Grunt](tasks/grunt.md)
Expand Down
46 changes: 46 additions & 0 deletions doc/tasks/git_branch_name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Git commit message

The Git branch name task ensures that the current branch name matches the specified patterns.
For example: if you are working with JIRA, it is possible to add a pattern for the JIRA issue number.

```yaml
# grumphp.yml
parameters:
tasks:
git_branch_name:
matchers:
Branch name nust contain JIRA issue number: /JIRA-\d+/
additional_modifiers: ''
```

**matchers**

*Default: []*

Use this parameter to specify one or multiple patterns. The value can be in regex or glob style.
Here are some example matchers:

- /JIRA-([0-9]*)/
- pre-fix*
- *suffix
- ...

**additional_modifiers**

*Default: ''*

Add one or multiple additional modifiers like:

```yaml
additional_modifiers: 'u'

# or

additional_modifiers: 'xu'
```

**allow_detached_head**

*Default: true*

Set this to `false` if you wish the task to fail when ran on a detached HEAD. If set to `true` the task will always pass.
8 changes: 8 additions & 0 deletions resources/config/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ services:
tags:
- {name: grumphp.task, config: git_commit_message}

task.git.branchname:
class: GrumPHP\Task\Git\BranchName
arguments:
- '@config'
- '@git.repository'
tags:
- {name: grumphp.task, config: git_branch_name}

task.git.conflict:
class: GrumPHP\Task\Git\Conflict
arguments:
Expand Down
109 changes: 109 additions & 0 deletions spec/Task/Git/BranchNameSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace spec\GrumPHP\Task\Git;

use Gitonomy\Git\Exception\ProcessException;
use Gitonomy\Git\Repository;
use GrumPHP\Configuration\GrumPHP;
use GrumPHP\Runner\TaskResultInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use GrumPHP\Task\Git\BranchName;
use GrumPHP\Task\TaskInterface;
use PhpSpec\ObjectBehavior;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BranchNameSpec extends ObjectBehavior
{
function let(GrumPHP $grumPHP, Repository $repository)
{
$this->beConstructedWith($grumPHP, $repository);
$grumPHP->getTaskConfiguration('git_branch_name')->willReturn([
'matchers' => ['test', '*es*', 'te[s][t]', '/^te(.*)/', '/(.*)st$/', '/t(e|a)st/', 'TEST'],
'additional_modifiers' => 'i',
]);
$repository->run('symbolic-ref', ['HEAD', '--short'])->willReturn('test');
}

function it_should_have_a_name()
{
$this->getName()->shouldBe('git_branch_name');
}

function it_should_have_configurable_options()
{
$options = $this->getConfigurableOptions();
$options->shouldBeAnInstanceOf(OptionsResolver::class);
$options->getDefinedOptions()->shouldContain('matchers');
$options->getDefinedOptions()->shouldContain('additional_modifiers');
}

function it_is_initializable()
{
$this->shouldHaveType(BranchName::class);
}

function it_is_a_grumphp_task()
{
$this->shouldImplement(TaskInterface::class);
}

function it_should_run_in_git_pre_commit_context(GitPreCommitContext $context)
{
$this->canRunInContext($context)->shouldReturn(true);
}

function it_should_run_in_run_context(RunContext $context)
{
$this->canRunInContext($context)->shouldReturn(true);
}

function it_runs_the_suite(RunContext $context)
{
$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->isPassed()->shouldBe(true);
}

function it_throws_exception_if_the_process_fails(RunContext $context, Repository $repository)
{
$repository->run('symbolic-ref', ['HEAD', '--short'])->willReturn('not-good');

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->isPassed()->shouldBe(false);
}

function it_runs_with_additional_modifiers(RunContext $context, GrumPHP $grumPHP, Repository $repository)
{
$grumPHP->getTaskConfiguration('git_branch_name')->willReturn([
'matchers' => ['/^ümlaut/'],
'additional_modifiers' => 'u',
]);

$repository->run('symbolic-ref', ['HEAD', '--short'])->willReturn('ümlaut-branch-name');

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->isPassed()->shouldBe(true);
}

function it_runs_with_detached_head_setting(RunContext $context, GrumPHP $grumPHP, Repository $repository)
{
$repository->run('symbolic-ref', ['HEAD', '--short'])->willThrow(ProcessException::class);

$grumPHP->getTaskConfiguration('git_branch_name')->willReturn([
'allow_detached_head' => true,
]);

$result = $this->run($context);
$result->isPassed()->shouldBe(true);

$grumPHP->getTaskConfiguration('git_branch_name')->willReturn([
'allow_detached_head' => false,
]);

$result = $this->run($context);
$result->isPassed()->shouldBe(false);
}
}
143 changes: 143 additions & 0 deletions src/Task/Git/BranchName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

namespace GrumPHP\Task\Git;

use Gitonomy\Git\Exception\ProcessException;
use GrumPHP\Runner\TaskResult;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use GrumPHP\Util\Regex;
use GrumPHP\Exception\RuntimeException;
use GrumPHP\Configuration\GrumPHP;
use GrumPHP\Task\TaskInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Gitonomy\Git\Repository;

/**
* Git BranchName Task
*/
class BranchName implements TaskInterface
{

/**
* @var GrumPHP
*/
protected $grumPHP;

/**
* @var Repository
*/
protected $repository;

/**
* @param GrumPHP $grumPHP
*/
public function __construct(GrumPHP $grumPHP, Repository $repository)
{
$this->grumPHP = $grumPHP;
$this->repository = $repository;
}

/**
* @return string
*/
public function getName()
{
return 'git_branch_name';
}

/**
* @return array
*/
public function getConfiguration()
{
$configured = $this->grumPHP->getTaskConfiguration($this->getName());

return $this->getConfigurableOptions()->resolve($configured);
}

/**
* @return OptionsResolver
*/
public function getConfigurableOptions()
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'matchers' => [],
'additional_modifiers' => '',
'allow_detached_head' => true,
]);

$resolver->addAllowedTypes('matchers', ['array']);
$resolver->addAllowedTypes('additional_modifiers', ['string']);
$resolver->addAllowedTypes('allow_detached_head', ['boolean']);

return $resolver;
}

/**
* @param ContextInterface $context
*
* @return bool
*/
public function canRunInContext(ContextInterface $context)
{
return $context instanceof RunContext || $context instanceof GitPreCommitContext;
}

/**
* @param array $config
* @param string $name
* @param string $rule
* @param string $ruleName
*
* @throws RuntimeException
*/
private function runMatcher(array $config, $name, $rule, $ruleName)
{
$regex = new Regex($rule);

$additionalModifiersArray = array_filter(str_split($config['additional_modifiers']));
array_map([$regex, 'addPatternModifier'], $additionalModifiersArray);

if (!preg_match((string) $regex, $name)) {
throw new RuntimeException("Rule not matched: \"$ruleName\" $rule");
}
}

/**
* @param ContextInterface|RunContext $context
*
* @return TaskResult
*/
public function run(ContextInterface $context)
{
$config = $this->getConfiguration();
$exceptions = [];

try {
$name = trim($this->repository->run('symbolic-ref', ['HEAD', '--short']));
} catch (ProcessException $e) {
if ($config['allow_detached_head']) {
return TaskResult::createPassed($this, $context);
}
$message = "Branch naming convention task is not allowed on a detached HEAD.";
return TaskResult::createFailed($this, $context, $message);
}

foreach ($config['matchers'] as $ruleName => $rule) {
try {
$this->runMatcher($config, $name, $rule, $ruleName);
} catch (RuntimeException $e) {
$exceptions[] = $e->getMessage();
}
}

if (count($exceptions)) {
return TaskResult::createFailed($this, $context, implode(PHP_EOL, $exceptions));
}

return TaskResult::createPassed($this, $context);
}
}