ktsu.GitIntegration
2.1.0
Prefix Reserved
See the version list below for details.
dotnet add package ktsu.GitIntegration --version 2.1.0
NuGet\Install-Package ktsu.GitIntegration -Version 2.1.0
<PackageReference Include="ktsu.GitIntegration" Version="2.1.0" />
<PackageVersion Include="ktsu.GitIntegration" Version="2.1.0" />
<PackageReference Include="ktsu.GitIntegration" />
paket add ktsu.GitIntegration --version 2.1.0
#r "nuget: ktsu.GitIntegration, 2.1.0"
#:package ktsu.GitIntegration@2.1.0
#addin nuget:?package=ktsu.GitIntegration&version=2.1.0
#tool nuget:?package=ktsu.GitIntegration&version=2.1.0
ktsu.GitIntegration
A .NET library that wraps the
gitbinary behind a fluent, strongly-typed interface, and unifies access to hosted Git providers behind a single abstraction.
Introduction
ktsu.GitIntegration is a two-layer library. The local layer wraps the git executable found
on PATH behind a fluent, strongly-typed interface: open or discover a repository, then build and
run status, log, diff, branches, remotes, and rev-parse commands without shelling out or
hand-parsing porcelain output yourself. The hosting layer — the original half of this library —
unifies access to hosted Git providers behind a GitProvider abstraction, with a GitHubProvider
implementation built on Octokit.
Every value that would otherwise be a bare string — a branch name, a commit SHA, a remote name, an
author email — is instead a validated semantic type built on ktsu.Semantics, so a GitBranchName
can no longer be accidentally passed where a GitCommitSha is expected.
Azure DevOps hosting support is planned but not yet implemented. The two Azure DevOps client
packages were deliberately left out of this release because they pull in System.Data.SqlClient,
which carries a known high-severity advisory, as a direct dependency of the published package.
Features
- Local Git Client:
IGitClient/GitClientfinds and opens repositories —GetVersionAsync,IsRepositoryAsync,OpenAsync,DiscoverAsync— by delegating every invocation toktsu.RunCommand. - Fluent Verb Builders:
GitRepositoryexposes one builder per read-only verb —Status(),Log(),Diff(),Branches(),Remotes(),RevParse(...)— each configurable via chained method calls and run withExecuteAsyncor the non-throwingTryExecuteAsync. - Strongly-Typed Results:
GitStatus,GitCommit,GitBranch,GitRemote,GitDiffEntry, andGitVersionrecords replace ad-hoc porcelain parsing with typed models. - Reproducible Failures: every command is scoped with
git -C <path>instead of a process working directory, so a failing invocation's exact argument vector can be read off aGitCommandExceptionand rerun verbatim. - Locale-Safe Parsing: every invocation runs with
GIT_TERMINAL_PROMPT=0(no hanging credential prompts) andLC_ALL=C(English, machine-stable output), which is what makes the output parsers safe to write against fixed English text. - Dependency Injection:
AddGitIntegration()registers the client, process runner, and options as singletons in one call. - Hosting Provider Abstraction:
GitProviderdefines a common contract for enumerating and refreshing remote repositories;GitHubProviderimplements it on top of Octokit. - Credential Resolution: hosting providers integrate with
ktsu.CredentialCache, so credentials come from the host's native keyring rather than configuration files. - Semantic Git Types: 13 validated wrapper types for every identifier Git tooling passes around, so mismatched arguments fail at compile time rather than at runtime.
Installation
Package Manager Console
Install-Package ktsu.GitIntegration
.NET CLI
dotnet add package ktsu.GitIntegration
Package Reference
<PackageReference Include="ktsu.GitIntegration" Version="x.y.z" />
Usage Examples
Basic Example
Register the library with dependency injection, then resolve IGitClient:
using ktsu.GitIntegration;
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
services.AddGitIntegration();
using ServiceProvider provider = services.BuildServiceProvider();
IGitClient client = provider.GetRequiredService<IGitClient>();
Discovering a Repository and Reading Its Status
using ktsu.GitIntegration;
using ktsu.Semantics.Paths;
using ktsu.Semantics.Strings;
AbsoluteDirectoryPath here = Environment.CurrentDirectory.As<AbsoluteDirectoryPath>();
GitRepository? repository = await client.DiscoverAsync(here);
if (repository is not null)
{
GitStatus status = await repository.Status().ExecuteAsync();
Console.WriteLine(status.IsClean
? "Working tree is clean."
: $"{status.Entries.Count} changed path(s) on {status.Branch?.WeakString}.");
}
Listing Commits and Diffs
using ktsu.GitIntegration;
using ktsu.Semantics.Strings;
IReadOnlyList<GitCommit> commits = await repository.Log()
.Take(10)
.FirstParentOnly()
.ExecuteAsync();
foreach (GitCommit commit in commits)
{
Console.WriteLine($"{commit.Sha.WeakString[..7]} {commit.Subject}");
}
IReadOnlyList<GitDiffEntry> changes = await repository.Diff()
.Staged()
.DetectRenames()
.ExecuteAsync();
Resolving a Revision Without Throwing
TryExecuteAsync reports a non-zero exit as a result instead of an exception — useful when "no such
revision" is an expected outcome rather than a failure:
using ktsu.GitIntegration;
using ktsu.Semantics.Strings;
GitResult<GitCommitSha> result = await repository
.RevParse("maybe-missing-branch".As<GitRefName>())
.TryExecuteAsync();
if (result.Success)
{
Console.WriteLine(result.Value!.WeakString);
}
else
{
Console.WriteLine($"git exited {result.Error!.ExitCode}: {result.Error.StandardError}");
}
Reproducing a Failing Command
ExecuteAsync throws GitCommandException on a non-zero exit, carrying the exact argument vector
git was invoked with:
try
{
await repository.RevParse("no-such-ref".As<GitRefName>()).ExecuteAsync();
}
catch (GitCommandException ex)
{
// ex.Arguments already begins with "-C <path>", so this can be pasted straight after `git`
// on a command line to reproduce the failure exactly.
Console.WriteLine("git " + string.Join(' ', ex.Arguments));
}
Working with a Hosting Provider
using ktsu.GitIntegration;
using ktsu.Semantics.Strings;
GitProvider provider = new GitHubProvider
{
Owner = "ktsu-dev".As<GitProviderOwner>(),
};
// Pulls credentials from the credential cache, then authenticates the client.
provider.RefreshRemoteRepositories();
Working with Semantic Types
using ktsu.GitIntegration;
using ktsu.Semantics.Strings;
GitBranchName branch = "main".As<GitBranchName>();
GitRemoteName remote = "origin".As<GitRemoteName>();
GitCommitSha sha = "9fceb02d0ae598e95dc970b74767f19372d61af8".As<GitCommitSha>();
// These are distinct types — passing a GitBranchName where a GitCommitSha
// is expected is a compile error, not a runtime surprise.
Advanced Usage
Argument Vectors Are Inspectable Before They Run
Every builder's BuildArguments() is a pure computation with no I/O, so the exact command can be
asserted or logged before it executes:
IReadOnlyList<string> arguments = repository.Status().BuildArguments();
// ["-C", "<path>", "--no-pager", "-c", "core.quotepath=false", "-c", "color.ui=false",
// "status", "--porcelain=v2", "--branch", "-z"]
Metadata-Only Repositories
A GitRepository produced by a hosting provider (rather than IGitClient.OpenAsync or
DiscoverAsync) carries hosting metadata but no ProcessRunner. Calling any verb on it throws
InvalidOperationException immediately, rather than failing later inside git:
GitRepository metadataOnly = new() { LocalPath = somePath, Name = "GitIntegration".As<GitRepositoryName>() };
// Throws InvalidOperationException — obtain a runnable repository from IGitClient first.
_ = metadataOnly.Status();
API Reference
IGitClient / GitClient
The entry point to the local layer: finds and opens repositories, and reports on the git binary.
Methods
| Name | Return Type | Description |
|---|---|---|
GetVersionAsync(CancellationToken) |
Task<GitVersion> |
Reports the version of the git binary being invoked. |
IsRepositoryAsync(AbsoluteDirectoryPath, CancellationToken) |
Task<bool> |
Decides whether a path is inside a git working tree. Never throws for a non-repository path. |
OpenAsync(AbsoluteDirectoryPath, CancellationToken) |
Task<GitRepository> |
Opens the repository containing a path. Throws GitRepositoryNotFoundException when there is none. |
DiscoverAsync(AbsoluteDirectoryPath, CancellationToken) |
Task<GitRepository?> |
Opens the repository containing a path, returning null instead of throwing when there is none. |
GitRepository
Carries LocalPath plus optional hosting metadata, and exposes one builder factory per verb.
Properties
| Name | Type | Description |
|---|---|---|
LocalPath |
AbsoluteDirectoryPath |
The working tree's local filesystem path. |
Name |
GitRepositoryName? |
The repository name, when known. |
WebURI |
GitRepositoryWebURI? |
The browser-facing URI, when known. |
RemotePath |
GitRepositoryRemotePath? |
The remote clone path, when known. |
ProcessRunner |
IGitProcessRunner? |
The runner this repository's verbs execute through; null on a metadata-only repository. |
Methods
| Name | Return Type | Description |
|---|---|---|
Status() |
IGitStatusBuilder |
Builds git status --porcelain=v2 --branch -z. |
Log() |
IGitLogBuilder |
Builds git log -z with this library's pinned format. |
Diff() |
IGitDiffBuilder |
Builds git diff --name-status -z. |
Branches() |
IGitBranchListBuilder |
Builds git for-each-ref over the branch namespaces. |
Remotes() |
IGitRemoteListBuilder |
Builds git remote -v. |
RevParse(GitRefName) |
IGitRevParseBuilder |
Builds git rev-parse --verify for a revision. |
IsClonedAsync(CancellationToken) |
Task<bool> |
Decides whether LocalPath currently holds a git working tree. |
OpenWebClient() |
void |
Opens WebURI in the default browser, when it is an absolute http/https URI. |
IGitCommandBuilder<TResult>
The shared contract every verb builder implements. A builder is single-use and not thread-safe.
Methods
| Name | Return Type | Description |
|---|---|---|
BuildArguments() |
IReadOnlyList<string> |
The exact argument vector this builder will pass to git. Pure, no I/O. |
ExecuteAsync(CancellationToken) |
Task<TResult> |
Runs the command, throwing GitCommandException when git exits non-zero. |
TryExecuteAsync(CancellationToken) |
Task<GitResult<TResult>> |
Runs the command, reporting a non-zero exit as a result instead of throwing. |
Verb Builders
| Interface | Extra Methods | Result |
|---|---|---|
IGitStatusBuilder |
WithUntrackedFiles(GitUntrackedFilesMode), IncludeIgnored() |
GitStatus |
IGitLogBuilder |
Take(int), Skip(int), ForRevision(GitRefName), ForPath(RelativeFilePath), FirstParentOnly() |
IReadOnlyList<GitCommit> |
IGitDiffBuilder |
Staged(), Against(GitRefName), Between(GitRefName, GitRefName), DetectRenames(), DetectCopies(), ForPath(RelativeFilePath) |
IReadOnlyList<GitDiffEntry> |
IGitBranchListBuilder |
LocalOnly(), RemoteOnly() |
IReadOnlyList<GitBranch> |
IGitRemoteListBuilder |
(none) | IReadOnlyList<GitRemote> |
IGitRevParseBuilder |
(none — revision supplied via GitRepository.RevParse) |
GitCommitSha |
Result and Execution Models
| Type | Description |
|---|---|
GitOptions |
Configures the git executable path and a per-invocation timeout. |
IGitProcessRunner |
Runs the git executable with a given argument vector; implemented by RunCommandGitProcessRunner. |
GitResult<T> |
The outcome of a command run with TryExecuteAsync: either Value or Error, never both. |
GitCommandError |
The exit code, argument vector, and standard error of a failed invocation. |
Exceptions
| Type | Thrown When |
|---|---|
GitException |
Base type for every failure originating in this library. |
GitExecutableNotFoundException |
The git executable could not be started. |
GitTimeoutException |
Git did not complete within the configured GitOptions.Timeout. |
GitParseException |
Git succeeded but produced output the parser could not interpret. |
GitCommandException |
Git ran and exited non-zero. Carries ExitCode, Arguments, and StandardError. |
GitRepositoryNotFoundException |
A GitCommandException specialization: the path is not inside a git working tree. |
Result Models
| Type | Description |
|---|---|
GitStatus |
Branch, Upstream, Ahead, Behind, IsDetached, Entries, IsClean. |
GitStatusEntry |
IndexState, WorkTreeState, Path, OriginalPath for one changed path. |
GitCommit |
Sha, TreeSha, ParentShas, Author, Committer, Subject, Body. |
GitSignature |
Name, Email, Timestamp recorded on a commit. |
GitBranch |
Name, Sha, Upstream, IsCurrent, IsRemote. |
GitRemote |
Name, FetchUrl, PushUrl. |
GitDiffEntry |
Kind, Path, OriginalPath, SimilarityPercent. |
GitVersion |
Major, Minor, Patch, Raw, plus AtLeast(major, minor). |
GitFileState |
Enum: Unmodified, Modified, Added, Deleted, Renamed, Copied, Untracked, Ignored, Unmerged, TypeChanged. |
GitChangeKind |
Enum: Added, Copied, Deleted, Modified, Renamed, TypeChanged, Unmerged, Unknown. |
GitUntrackedFilesMode |
Enum: No, Normal, All. |
GitProvider
Abstract base class describing a hosted Git provider.
Properties
| Name | Type | Description |
|---|---|---|
Name |
GitProviderName |
Display name of the provider. |
Owner |
GitProviderOwner |
The owner of the repositories in this provider. |
PersonaGUID |
PersonaGUID |
The persona GUID used for authentication with the provider (from ktsu.CredentialCache). |
IsAuthenticated |
bool |
Whether a credential is currently resolvable for this provider. |
Repositories |
ConcurrentBag<GitRepository> |
The repositories known from the provider. |
Methods
| Name | Return Type | Description |
|---|---|---|
RefreshRemoteRepositories() |
void |
Refreshes the provider's view of the remote repositories, authenticating first if a credential is available. |
TryGetCredential(out Credential?) |
bool |
Attempts to resolve a credential for this provider from the credential cache. |
GitHubProvider
GitProvider implementation backed by Octokit. Authenticates the underlying GitHubClient from a
CredentialWithUsernamePassword resolved via TryGetCredential.
ServiceCollectionExtensions
| Name | Return Type | Description |
|---|---|---|
AddGitIntegration(IServiceCollection) |
IServiceCollection |
Registers git integration with default options, invoking the git found on PATH. |
AddGitIntegration(IServiceCollection, Action<GitOptions>) |
IServiceCollection |
Registers git integration with configured options. Idempotent per service. |
Semantic Types
| Type | Wraps |
|---|---|
GitAuthorEmail |
Commit author or committer email address |
GitAuthorName |
Commit author or committer name |
GitBranchName |
Branch name |
GitCommitMessage |
Commit message |
GitCommitSha |
Commit object id (abbreviated or full, including SHA-256 repositories) |
GitProviderName |
Hosting provider display name |
GitProviderOwner |
Account or organization owning a repository |
GitRefName |
A branch, tag, SHA, or revision expression |
GitRemoteName |
Remote name |
GitRepositoryName |
Repository name |
GitRepositoryRemotePath |
Clone path or URL |
GitRepositoryWebURI |
Repository web address |
AzureDevOpsProjectName |
Azure DevOps project name (reserved for planned Azure DevOps support) |
Contributing
Contributions are welcome! Feel free to open issues or submit pull requests.
License
This project is licensed under the MIT License. See the LICENSE.md file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ktsu.CredentialCache (>= 1.3.21)
- ktsu.Essentials (>= 2.0.0)
- ktsu.Essentials.FileSystemProviders.Native (>= 2.0.0)
- ktsu.RunCommand (>= 1.5.0)
- ktsu.Semantics.Paths (>= 3.0.1)
- ktsu.Semantics.Strings (>= 3.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.2)
- Octokit (>= 14.0.0)
-
net9.0
- ktsu.CredentialCache (>= 1.3.21)
- ktsu.Essentials (>= 2.0.0)
- ktsu.Essentials.FileSystemProviders.Native (>= 2.0.0)
- ktsu.RunCommand (>= 1.5.0)
- ktsu.Semantics.Paths (>= 3.0.1)
- ktsu.Semantics.Strings (>= 3.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.2)
- Octokit (>= 14.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.3 | 95 | 8/26/2026 |
| 2.5.2 | 100 | 8/26/2026 |
| 2.5.1 | 92 | 8/25/2026 |
| 2.5.0 | 94 | 8/25/2026 |
| 2.4.0 | 258 | 8/20/2026 |
| 2.3.0 | 99 | 8/20/2026 |
| 2.2.1 | 103 | 8/20/2026 |
| 2.2.0 | 100 | 8/20/2026 |
| 2.1.0 | 108 | 8/20/2026 |
| 2.0.0 | 94 | 8/19/2026 |
| 1.1.9 | 92 | 8/19/2026 |
| 1.1.8 | 95 | 8/18/2026 |
| 1.1.7 | 111 | 8/17/2026 |
| 1.1.6 | 100 | 8/11/2026 |
| 1.1.5 | 94 | 8/6/2026 |
| 1.1.4 | 102 | 8/5/2026 |
| 1.1.3 | 129 | 6/28/2026 |
| 1.1.2 | 112 | 6/28/2026 |
| 1.1.2-pre.18 | 227 | 5/20/2025 |
| 1.1.2-pre.15 | 175 | 5/17/2025 |
## v2.1.0 (minor)
Changes since v2.0.0:
- [patch] Rewrite docs for the two-layer local git client + hosting provider library ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Apply final whole-branch review fixes ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add IGitClient, GitClient, and the read-only repository verbs ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the rev-parse verb and the fixed-vector text builder ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the remote listing verb ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the branch listing verb ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Rename Between parameters to fromRevision and toRevision ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the diff verb builder ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the name-status diff parser ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the log verb builder ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the log parser ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Fix NUL escape format to match Task 3 convention ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the status verb builder ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the porcelain v2 status parser ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add the git --version verb ([@matt-edmondson](https://github.com/matt-edmondson))
- [minor] Add read-only result models and parsing primitives ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Correct the documented cancellation contract ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Document that the progress sink must be thread-safe ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Fix two plan defects found in the pre-flight cross-task scan ([@matt-edmondson](https://github.com/matt-edmondson))
- [patch] Add the Phase 3 read-only verbs implementation plan ([@matt-edmondson](https://github.com/matt-edmondson))