Pipette.Git (Pipette v0.6.0)

Copy Markdown View Source

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

Summary

Functions

Check if all changed files match the ignore patterns.

Determine the base commit for git diff comparison.

Get the list of files changed since the base commit.

Determine which scopes are fired by the given changed files.

Check if a file path matches a glob pattern.

Functions

all_ignored?(changed_files, ignore_patterns)

@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(ctx)

@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(base, opts \\ [])

@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(scopes, changed_files)

@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?(file_path, pattern)

@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