Git Package
Git repository information, versioning, and strongly typed Git commands.
Installation
dotnet add package ModularPipelines.Git
Required command-line tool: git. It must be installed and available on PATH when the pipeline runs.
Context entry points
Use the discoverable context.Tools surface from a module:
context.Tools.Git
Module example
using ModularPipelines;
using ModularPipelines.Git.Options;
public class UseGitModule : Module<CommandResult>
{
protected override async Task<CommandResult> ExecuteAsync(
IModuleContext context,
CancellationToken cancellationToken)
{
return await context.Tools.Git.Commands.WorkingTree.StatusAsync(
new GitStatusOptions
{
Short = true,
},
cancellationToken: cancellationToken);
}
}
The package exposes strongly typed options records for its supported CLI commands.
Shared execution options
All command options inherit root execution settings from GitOptions. These settings
appear before the command name. For example:
await context.Tools.Git.Commands.WorkingTree.StatusAsync(new GitStatusOptions
{
ChangeDirectories = ["checkout", "application"],
Configuration = [("core.quotepath", "false")],
NoOptionalLocks = true,
Short = true,
}, cancellationToken: cancellationToken);
ChangeDirectories repeats -C in collection order. Each relative directory resolves
against the previous directory; directory changes precede other root settings.
Configuration repeats -c key=value without collapsing duplicate keys. Use an empty
value to clear a setting or "true" for Git's implicit boolean form. Values use KeyValue
and are registered for secret masking, including HTTP authorization headers.
Secret masking does not remove values from Git's process arguments. Users able to
inspect those arguments may read credentials. Use ConfigEnv for credential-bearing
values that must not appear in Git's arguments.
ConfigEnv repeats --config-env=name=ENVIRONMENT_VARIABLE; it contains variable names,
not their secret contents.
GitDirectory, WorkTree, Namespace, and ExecPath use equals-separated values.
ExecPath requires a value; its bare path-reporting form is not an inherited setting.
Pager controls, replacement-object controls, optional-lock controls, pathspec settings,
AttrSource, NoLazyFetch, and NoAdvice are also shared. The installed Git version
must support each selected option. Command-local switches retain their separate scope:
GitRevParseOptions.GitDir reports a path, while GitDirectory selects the repository
before rev-parse executes. BareRepository sets root --bare; command-local Bare
properties such as GitCloneOptions.Bare still follow their command.
GitBaseOptions retains version and path-reporting actions for Repository.GitAsync.
These actions are not inherited by ordinary commands. Existing GitBaseOptions.GitDir
and .Bare initializers migrate to .GitDirectory and .BareRepository respectively;
other shared properties retain their names through inheritance.
The Git integration deliberately retains its handwritten grouped facade and option
ownership. Its scraper captures root usage separately from git help -a, validates the
complete command tree, and refreshes coverage metadata without replacing these records.
See the Git command reference for root-option semantics.
Repository information
When the pipeline always runs inside a repository, use GetRequiredInfoAsync. It throws an
InvalidOperationException when Git information is unavailable, for example outside a repository or
when git is not installed:
var repository = await context.Tools.Git.Information.GetRequiredInfoAsync(cancellationToken);
var branch = repository.BranchName;
var commit = repository.LastCommitSha;
Properties such as BranchName and LastCommitSha remain nullable, for example on a detached HEAD
or in a repository without commits. Use GetInfoAsync when the pipeline must handle running outside a
repository; it returns null instead of throwing.
Version calculation
Use context.Tools.Git.Versioning.GetVersioningInformationAsync(cancellationToken) to calculate
the repository version with GitVersion.Tool. The integration installs version 6.8.2 in a temporary
folder. A .NET SDK and access to the package feed are required when the version is first requested.
To select another exact tool version, configure the integration during service registration:
using ModularPipelines.Git.Extensions;
builder.Services.RegisterGitContext(options => options.ToolVersion = "6.8.2");
GitVersion uses an existing GitVersion.yml, GitVersion.yaml, .GitVersion.yml, or
.GitVersion.yaml in the repository root. When none exists, the integration passes a temporary
configuration with mode: ContinuousDeployment and the Mainline strategy. It does not create
or modify repository configuration, and disables GitVersion's disk cache. The temporary tool
and fallback configuration are deleted after calculation, including when calculation fails or
is canceled.
Results are cached per integration instance (the pipeline's dependency injection scope). Concurrent requests on that instance share one calculation. Separate instances calculate independently and never reuse another repository's result.
Run only when paths change
Use RunIfChangedAttribute to run a module when at least one repository-relative glob matches a
path changed since the merge base with origin/main:
using ModularPipelines.Git.Attributes;
[RunIfChanged("src/MyService/**", "test/MyService.Tests/**")]
public class TestMyServiceModule : Module<CommandResult>
{
// ...
}
Set another base revision with the named Base property:
[RunIfChanged("src/**", Base = "origin/release")]
For imperative checks, use the same cached changed-path set through the Git context:
var shouldBuild = await context.Tools.Git.Changes.HasChangesAsync(
["src/MyService/**", "Directory.Packages.props"],
cancellationToken: cancellationToken);
Each base revision is resolved with git merge-base, and its git diff --name-only result is
combined with untracked, non-ignored files once per pipeline run. The comparison therefore includes
committed, staged, unstaged, and new files. A pattern without * or ? matches both that exact path
and paths beneath it, so src/MyService can be used instead of src/MyService/**. If the base
revision is unavailable (for example, in a shallow checkout without origin/main), the condition
logs a warning and conservatively runs the module. The same fallback applies when the merge base
is HEAD, including push builds where origin/main already points at the checked-out commit or
HEAD is an ancestor of the base. There is no earlier comparison point in that case, so every guarded
module runs. Comparing only the previous commit could miss changes from a multi-commit push.
For pull requests in GitHub Actions, fetch the base branch and enough history to include the common ancestor. The default shallow checkout may not contain either. Fetching all history provides both:
- uses: actions/checkout@v4
with:
fetch-depth: 0
A shallow checkout is safe, but an unavailable merge base makes the condition run every guarded module instead of filtering by paths.
Custom command runners
If you replace IGitCommandRunner and use changed-path checks, implement
IRawGitCommandRunner on the same class. GitChanges uses its untrimmed output to preserve
NUL-delimited Git paths exactly, including leading or trailing whitespace.
builder.Services.AddSingleton<IGitCommandRunner, CustomGitCommandRunner>();
CustomGitCommandRunner must implement both interfaces on the same class.