# `Pipette.Git`
[🔗](https://github.com/tommeier/pipette-buildkite-plugin/blob/main/lib/pipette/git.ex#L1)

Git change detection and glob matching for pipeline activation.

Provides utilities to determine which files changed between commits,
match file paths against glob patterns, and resolve which scopes
should be activated based on changed files.

## Glob Matching Rules

Patterns use `**` and `*` wildcards:

  * `**` — matches any number of path segments (including zero), e.g.
    `"apps/api/**"` matches `"apps/api/lib/user.ex"`
  * `*` — matches anything except `/`, e.g. `"*.md"` matches `"README.md"`
  * Patterns without `/` also match against the file's basename, so
    `"*.md"` matches both `"README.md"` and `"docs/guide.md"`

## Base Commit Resolution

The base commit for `git diff` is determined by priority:

  1. PR base branch (`BUILDKITE_PULL_REQUEST_BASE_BRANCH`) — `origin/<base>`
  2. Non-default branch — `origin/<default_branch>`
  3. Default branch — `HEAD~1`

## Examples

    base = Pipette.Git.base_commit(ctx)
    {:ok, files} = Pipette.Git.changed_files(base)

    fired = Pipette.Git.fired_scopes(scopes, files)

    Pipette.Git.matches_glob?("apps/api/lib/user.ex", "apps/api/**")
    #=> true

    Pipette.Git.matches_glob?("README.md", "*.md")
    #=> true

# `all_ignored?`

```elixir
@spec all_ignored?([String.t()], [String.t()]) :: boolean()
```

Check if all changed files match the ignore patterns.

Returns `true` when every file in the list matches at least one ignore
pattern. Returns `false` for an empty file list (no changes means not ignored).

## Examples

    Pipette.Git.all_ignored?(["README.md", "docs/guide.md"], ["*.md", "docs/**"])
    #=> true

    Pipette.Git.all_ignored?(["README.md", "apps/api/lib/user.ex"], ["*.md"])
    #=> false

    Pipette.Git.all_ignored?([], ["*.md"])
    #=> false

# `base_commit`

```elixir
@spec base_commit(Pipette.Context.t()) :: String.t()
```

Determine the base commit for `git diff` comparison.

Resolution order:
1. PR base branch — `origin/<BUILDKITE_PULL_REQUEST_BASE_BRANCH>`
2. Non-default branch — `origin/<default_branch>`
3. Default branch — `HEAD~1`

## Examples

    iex> ctx = %Pipette.Context{pull_request_base_branch: "main", branch: "feature/x", default_branch: "main"}
    iex> Pipette.Git.base_commit(ctx)
    "origin/main"

    iex> ctx = %Pipette.Context{pull_request_base_branch: nil, branch: "main", default_branch: "main"}
    iex> Pipette.Git.base_commit(ctx)
    "HEAD~1"

# `changed_files`

```elixir
@spec changed_files(
  String.t(),
  keyword()
) :: {:ok, [String.t()]} | {:error, String.t()}
```

Get the list of files changed since the base commit.

Runs `git diff --name-only <base>` and returns the list of file paths.
Accepts a `:runner` option for testing (a function that takes the base
commit and returns `{:ok, output}` or `{:error, reason}`).

## Examples

    {:ok, files} = Pipette.Git.changed_files("origin/main")
    files  #=> ["apps/api/lib/user.ex", "apps/web/src/App.tsx"]

# `fired_scopes`

```elixir
@spec fired_scopes([Pipette.Scope.t()], [String.t()]) :: MapSet.t(atom())
```

Determine which scopes are fired by the given changed files.

A scope fires when any changed file matches at least one of its `files`
patterns and none of its `exclude` patterns.

## Examples

    scopes = [
      %Pipette.Scope{name: :api_code, files: ["apps/api/**"]},
      %Pipette.Scope{name: :web_code, files: ["apps/web/**"]}
    ]

    fired = Pipette.Git.fired_scopes(scopes, ["apps/api/lib/user.ex"])
    fired  #=> MapSet.new([:api_code])

# `matches_glob?`

```elixir
@spec matches_glob?(String.t(), String.t()) :: boolean()
```

Check if a file path matches a glob pattern.

Patterns without `/` also match against the file's basename.

## Examples

    iex> Pipette.Git.matches_glob?("apps/api/lib/user.ex", "apps/api/**")
    true

    iex> Pipette.Git.matches_glob?("README.md", "*.md")
    true

    iex> Pipette.Git.matches_glob?("apps/api/lib/user.ex", "apps/web/**")
    false

---

*Consult [api-reference.md](api-reference.md) for complete listing*
