defmodule GitGud.Workflows.Glob do
@moduledoc """
File path patterns as workflows write them under `paths:` and
`paths-ignore:`, with GitHub's rules (its "filter pattern cheat sheet"),
because that is the documentation the patterns are copied from:
* `*` matches any run of characters except `/`
* `**` matches any run of characters including `/`; `**/` also matches
no directory at all, so `**/*.md` matches `README.md`
* `?` matches one character except `/`
* `[abc]`, `[a-z]` are character classes
* everything else is literal — `.` is a dot
Patterns are relative to the repository root and anchored at both ends.
A leading `!` (negation) is the caller's business: it means something
only in a list of patterns, see `GitGud.Workflows.Yaml`.
Not used for `branches:` / `tags:`, whose `*` still crosses `/`; making
those follow the same rule would change which existing filters fire.
"""
import Kernel, except: [match?: 2]
@doc """
Compile `pattern` into an anchored regex.
iex> Regex.source(GitGud.Workflows.Glob.compile("docs/*.md"))
"^docs/[^/]*\\\\.md$"
"""
@spec compile(String.t()) :: Regex.t()
def compile(pattern) when is_binary(pattern) do
Regex.compile!("^" <> translate(pattern, "") <> "$")
end
@doc """
Whether `path` matches `pattern` (a string, or a regex from `compile/1`).
iex> GitGud.Workflows.Glob.match?("**/*.md", "README.md")
true
"""
@spec match?(String.t() | Regex.t(), String.t()) :: boolean()
def match?(%Regex{} = regex, path) when is_binary(path), do: Regex.match?(regex, path)
def match?(pattern, path) when is_binary(pattern), do: Regex.match?(compile(pattern), path)
# `**/` — zero or more whole directories.
defp translate("**/" <> rest, acc), do: translate(rest, acc <> "(?:.*/)?")
# `**` anywhere else (`docs/**`, `**.js`, bare `**`) — anything at all.
defp translate("**" <> rest, acc), do: translate(rest, acc <> ".*")
defp translate("*" <> rest, acc), do: translate(rest, acc <> "[^/]*")
defp translate("?" <> rest, acc), do: translate(rest, acc <> "[^/]")
defp translate("[" <> rest, acc) do
case class(rest, "") do
{:ok, body, rest} -> translate(rest, acc <> "[" <> body <> "]")
# No closing bracket: the `[` was just a character.
:error -> translate(rest, acc <> "\\[")
end
end
defp translate(<<c::utf8, rest::binary>>, acc),
do: translate(rest, acc <> Regex.escape(<<c::utf8>>))
defp translate("", acc), do: acc
# The inside of `[...]`: ranges (`-`) and a leading `^` keep their regex
# meaning; anything else that a regex class would read specially is escaped.
defp class("]" <> rest, body) when body not in ["", "^"], do: {:ok, body, rest}
defp class("-" <> rest, body), do: class(rest, body <> "-")
defp class("^" <> rest, ""), do: class(rest, "^")
defp class(<<c::utf8, rest::binary>>, body) when c in [?\\, ?[, ?], ?^],
do: class(rest, body <> "\\" <> <<c::utf8>>)
defp class(<<c::utf8, rest::binary>>, body), do: class(rest, body <> <<c::utf8>>)
defp class("", _body), do: :error
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
3.0 KiB · text
History
34b2c06