mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 23:20:43 +00:00
feat(application): implement order-based pattern matching for watch paths with negation support
This commit is contained in:
@@ -1556,40 +1556,185 @@ class Application extends BaseModel
|
||||
return $command;
|
||||
}
|
||||
|
||||
private function parseWatchPaths($value)
|
||||
{
|
||||
if ($value) {
|
||||
$watch_paths = collect(explode("\n", $value))
|
||||
->map(function (string $path): string {
|
||||
// Trim whitespace and remove leading slashes to normalize paths
|
||||
$path = trim($path);
|
||||
|
||||
return ltrim($path, '/');
|
||||
})
|
||||
->filter(function (string $path): bool {
|
||||
return strlen($path) > 0;
|
||||
});
|
||||
|
||||
return trim($watch_paths->implode("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
public function watchPaths(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if ($value) {
|
||||
return trim($value);
|
||||
return $this->parseWatchPaths($value);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function matchWatchPaths(Collection $modified_files, ?Collection $watch_paths): Collection
|
||||
{
|
||||
return self::matchPaths($modified_files, $watch_paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to match paths against watch patterns with negation support
|
||||
* Uses order-based matching: last matching pattern wins
|
||||
*/
|
||||
public static function matchPaths(Collection $modified_files, ?Collection $watch_paths): Collection
|
||||
{
|
||||
if (is_null($watch_paths) || $watch_paths->isEmpty()) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
return $modified_files->filter(function ($file) use ($watch_paths) {
|
||||
$shouldInclude = null; // null means no patterns matched
|
||||
|
||||
// Process patterns in order - last match wins
|
||||
foreach ($watch_paths as $pattern) {
|
||||
$pattern = trim($pattern);
|
||||
if (empty($pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isExclusion = str_starts_with($pattern, '!');
|
||||
$matchPattern = $isExclusion ? substr($pattern, 1) : $pattern;
|
||||
|
||||
if (self::globMatch($matchPattern, $file)) {
|
||||
// This pattern matches - it determines the current state
|
||||
$shouldInclude = ! $isExclusion;
|
||||
}
|
||||
}
|
||||
|
||||
// If no patterns matched and we only have exclusion patterns, include by default
|
||||
if ($shouldInclude === null) {
|
||||
// Check if we only have exclusion patterns
|
||||
$hasInclusionPatterns = $watch_paths->contains(fn ($p) => ! str_starts_with(trim($p), '!'));
|
||||
|
||||
return ! $hasInclusionPatterns;
|
||||
}
|
||||
|
||||
return $shouldInclude;
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path matches a glob pattern
|
||||
* Supports: *, **, ?, [abc], [!abc]
|
||||
*/
|
||||
public static function globMatch(string $pattern, string $path): bool
|
||||
{
|
||||
$regex = self::globToRegex($pattern);
|
||||
|
||||
return preg_match($regex, $path) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a glob pattern to a regular expression
|
||||
*/
|
||||
public static function globToRegex(string $pattern): string
|
||||
{
|
||||
$regex = '';
|
||||
$inGroup = false;
|
||||
$chars = str_split($pattern);
|
||||
$len = count($chars);
|
||||
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$c = $chars[$i];
|
||||
|
||||
switch ($c) {
|
||||
case '*':
|
||||
// Check for **
|
||||
if ($i + 1 < $len && $chars[$i + 1] === '*') {
|
||||
// ** matches any number of directories
|
||||
$regex .= '.*';
|
||||
$i++; // Skip next *
|
||||
// Skip optional /
|
||||
if ($i + 1 < $len && $chars[$i + 1] === '/') {
|
||||
$i++;
|
||||
}
|
||||
} else {
|
||||
// * matches anything except /
|
||||
$regex .= '[^/]*';
|
||||
}
|
||||
break;
|
||||
|
||||
case '?':
|
||||
// ? matches any single character except /
|
||||
$regex .= '[^/]';
|
||||
break;
|
||||
|
||||
case '[':
|
||||
// Character class
|
||||
$inGroup = true;
|
||||
$regex .= '[';
|
||||
// Check for negation
|
||||
if ($i + 1 < $len && ($chars[$i + 1] === '!' || $chars[$i + 1] === '^')) {
|
||||
$regex .= '^';
|
||||
$i++;
|
||||
}
|
||||
break;
|
||||
|
||||
case ']':
|
||||
if ($inGroup) {
|
||||
$inGroup = false;
|
||||
$regex .= ']';
|
||||
} else {
|
||||
$regex .= preg_quote($c, '#');
|
||||
}
|
||||
break;
|
||||
|
||||
case '.':
|
||||
case '(':
|
||||
case ')':
|
||||
case '+':
|
||||
case '{':
|
||||
case '}':
|
||||
case '$':
|
||||
case '^':
|
||||
case '|':
|
||||
case '\\':
|
||||
// Escape regex special characters
|
||||
$regex .= '\\'.$c;
|
||||
break;
|
||||
|
||||
default:
|
||||
$regex .= $c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap in delimiters and anchors
|
||||
return '#^'.$regex.'$#';
|
||||
}
|
||||
|
||||
public function isWatchPathsTriggered(Collection $modified_files): bool
|
||||
{
|
||||
if (is_null($this->watch_paths)) {
|
||||
return false;
|
||||
}
|
||||
$watch_paths = collect(explode("\n", $this->watch_paths))
|
||||
->map(function (string $path): string {
|
||||
return trim($path);
|
||||
})
|
||||
->filter(function (string $path): bool {
|
||||
return strlen($path) > 0;
|
||||
});
|
||||
$this->watch_paths = $this->parseWatchPaths($this->watch_paths);
|
||||
$this->save();
|
||||
$watch_paths = collect(explode("\n", $this->watch_paths));
|
||||
|
||||
// If no valid patterns after filtering, don't trigger
|
||||
if ($watch_paths->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$matches = $modified_files->filter(function ($file) use ($watch_paths) {
|
||||
return $watch_paths->contains(function ($glob) use ($file) {
|
||||
return fnmatch($glob, $file);
|
||||
});
|
||||
});
|
||||
$matches = $this->matchWatchPaths($modified_files, $watch_paths);
|
||||
|
||||
return $matches->count() > 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user