mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
refactor: standalone regex matching for commit skip patterns
Replace directive-extraction approach with word-boundary regex matching so plain-word patterns like "skip appwrite" and "appwrite skip" work alongside bracket directives. Use \s+ between word tokens (required space) and \s* only after ":" tokens (git trailer flexibility). Add tests for "skip appwrite" and "appwrite skip" with case insensitivity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
4a4f51622d
commit
444d4b4e66
@@ -12,8 +12,16 @@ class CommitSkipPatterns extends Validator
|
||||
|
||||
/**
|
||||
* Returns false (skip deployment) when the commit message contains any of the
|
||||
* configured skip directives.
|
||||
* configured patterns as a standalone directive (case-insensitive).
|
||||
* Returns true (proceed) when no patterns are configured or none match.
|
||||
*
|
||||
* Matching rules:
|
||||
* - Case-insensitive
|
||||
* - The directive must be surrounded by whitespace or string boundaries, so
|
||||
* "prefix[skip deploy]suffix" does NOT accidentally skip
|
||||
* - Internal whitespace in the pattern is normalised: tokens are split on \s+
|
||||
* and rejoined with \s* in the regex, so "[skip deploy]" matches
|
||||
* "[skip deploy]" and "skip-checks: true" matches "skip-checks:true"
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
@@ -21,13 +29,38 @@ class CommitSkipPatterns extends Validator
|
||||
return false;
|
||||
}
|
||||
|
||||
$patterns = $this->normalizePatterns($this->patterns);
|
||||
if (empty($patterns)) {
|
||||
return true;
|
||||
}
|
||||
foreach ($this->patterns as $pattern) {
|
||||
if (!is_string($pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->extractDirectives($value) as $directive) {
|
||||
if (isset($patterns[$directive])) {
|
||||
$pattern = trim($pattern);
|
||||
if ($pattern === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split on whitespace; each token is regex-quoted. Tokens are rejoined
|
||||
// with \s+ (required space) so that "skipappwrite" does NOT match the
|
||||
// pattern "skip appwrite". The only exception: when the preceding token
|
||||
// ends with ":" (git trailer style), \s* is used so that
|
||||
// "skip-checks:true" still matches the pattern "skip-checks: true".
|
||||
$tokens = preg_split('/\s+/', $pattern);
|
||||
$regexParts = [];
|
||||
$count = count($tokens);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$regexParts[] = preg_quote($tokens[$i], '~');
|
||||
if ($i < $count - 1) {
|
||||
$regexParts[] = str_ends_with($tokens[$i], ':') ? '\s*' : '\s+';
|
||||
}
|
||||
}
|
||||
$regexBody = implode('', $regexParts);
|
||||
|
||||
// (?<!\S) / (?!\S) assert whitespace (or string edge) on both sides,
|
||||
// ensuring the directive is a standalone group, not buried inside a
|
||||
// longer token like "prefix[skip deploy]suffix".
|
||||
$regex = '~(?<!\S)' . $regexBody . '(?!\S)~i';
|
||||
|
||||
if (preg_match($regex, $value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -49,64 +82,4 @@ class CommitSkipPatterns extends Validator
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $patterns
|
||||
* @return array<string, true>
|
||||
*/
|
||||
private function normalizePatterns(array $patterns): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (!\is_string($pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = $this->normalizeDirective($pattern);
|
||||
if ($pattern === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized[$pattern] = true;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function extractDirectives(string $message): array
|
||||
{
|
||||
$directives = [];
|
||||
|
||||
if (\preg_match_all('/\[[^\]\r\n]+\]/u', $message, $matches) > 0) {
|
||||
foreach ($matches[0] as $match) {
|
||||
$directives[] = $this->normalizeDirective($match);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (\preg_split("/\r\n|\n|\r/", $message) ?: [] as $line) {
|
||||
$line = \trim($line);
|
||||
if ($line === '' || !\str_contains($line, ':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$directives[] = $this->normalizeDirective($line);
|
||||
}
|
||||
|
||||
return \array_values(\array_filter(\array_unique($directives)));
|
||||
}
|
||||
|
||||
private function normalizeDirective(string $value): string
|
||||
{
|
||||
$value = \trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = (string) \preg_replace('/\s+/u', ' ', $value);
|
||||
return \mb_strtolower($value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,4 +151,65 @@ class CommitSkipPatternsTest extends TestCase
|
||||
$msg = "feat: add new stuff\n\nMore detail here.\n\nskip-checks:true";
|
||||
$this->assertFalse($validator->isValid($msg));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Plain-word patterns: "skip appwrite" and "appwrite skip"
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testSkipAppwritePatternSkips(): void
|
||||
{
|
||||
$validator = new CommitSkipPatterns(['skip appwrite']);
|
||||
$this->assertFalse($validator->isValid('docs: update readme skip appwrite'));
|
||||
$this->assertFalse($validator->isValid('skip appwrite'));
|
||||
}
|
||||
|
||||
public function testSkipAppwritePatternCaseInsensitive(): void
|
||||
{
|
||||
$validator = new CommitSkipPatterns(['skip appwrite']);
|
||||
$this->assertFalse($validator->isValid('SKIP APPWRITE'));
|
||||
$this->assertFalse($validator->isValid('Skip Appwrite'));
|
||||
$this->assertFalse($validator->isValid('SKIP appwrite'));
|
||||
}
|
||||
|
||||
public function testSkipAppwritePatternNoMatchProceeds(): void
|
||||
{
|
||||
$validator = new CommitSkipPatterns(['skip appwrite']);
|
||||
$this->assertTrue($validator->isValid('feat: real feature'));
|
||||
$this->assertTrue($validator->isValid('skipappwrite')); // no space — not standalone
|
||||
$this->assertTrue($validator->isValid('appwrite is great'));
|
||||
}
|
||||
|
||||
public function testAppwriteSkipPatternSkips(): void
|
||||
{
|
||||
$validator = new CommitSkipPatterns(['appwrite skip']);
|
||||
$this->assertFalse($validator->isValid('appwrite skip ci'));
|
||||
$this->assertFalse($validator->isValid('docs appwrite skip'));
|
||||
$this->assertFalse($validator->isValid('appwrite skip'));
|
||||
}
|
||||
|
||||
public function testAppwriteSkipPatternCaseInsensitive(): void
|
||||
{
|
||||
$validator = new CommitSkipPatterns(['appwrite skip']);
|
||||
$this->assertFalse($validator->isValid('APPWRITE SKIP'));
|
||||
$this->assertFalse($validator->isValid('Appwrite Skip'));
|
||||
$this->assertFalse($validator->isValid('appwrite SKIP'));
|
||||
}
|
||||
|
||||
public function testAppwriteSkipPatternNoMatchProceeds(): void
|
||||
{
|
||||
$validator = new CommitSkipPatterns(['appwrite skip']);
|
||||
$this->assertTrue($validator->isValid('feat: deploy appwrite changes'));
|
||||
$this->assertTrue($validator->isValid('appwriteskip')); // no space — not standalone
|
||||
$this->assertTrue($validator->isValid('skip the appwrite stuff'));
|
||||
}
|
||||
|
||||
public function testBothAppwritePatternsInArray(): void
|
||||
{
|
||||
$validator = new CommitSkipPatterns(['skip appwrite', 'appwrite skip']);
|
||||
$this->assertFalse($validator->isValid('skip appwrite'));
|
||||
$this->assertFalse($validator->isValid('appwrite skip'));
|
||||
$this->assertFalse($validator->isValid('SKIP APPWRITE'));
|
||||
$this->assertFalse($validator->isValid('APPWRITE SKIP'));
|
||||
$this->assertTrue($validator->isValid('feat: deploy appwrite changes'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user