Typed .NET API for the jujutsu (jj) version control system.
Built on ProcessKit for safe child-process
lifetime guarantees and async streaming.
Pre-1.0. The workflow-complete API (status, log, diff, show, new, describe,
edit, abandon, restore, squash, rebase, git fetch/push/clone, op log/undo/restore,
bookmark list/set/delete) is in place. See CHANGELOG.md for what's covered.
- .NET 10.0 or later
- A working
jjbinary onPATH(or an explicit path viaJjOptions.JjExecutable) - AOT-compatible
dotnet add package JjRun(Available on NuGet.org once released.)
using JjRun;
using ProcessKit;
var jj = new Jj(ProcessRunner.Default, new JjOptions { WorkingDirectory = "/path/to/repo" });
// Describe the current change
await jj.DescribeAsync("Implement feature X");
// Create a follow-up change
await jj.NewAsync(message: "Tests for feature X");
// Stream the log as typed records (parsed from a private template — never human output)
await foreach (var entry in jj.LogAsync(revisions: "main..@", limit: 50))
Console.WriteLine($"{entry.ChangeId} {entry.AuthorName} {entry.DescriptionFirstLine}");
// Sync with the remote
await jj.Git.FetchAsync(remote: "origin");
await jj.RebaseAsync(revision: "@-", destination: "main@origin");
await jj.Bookmark.SetAsync("main", revision: "@-");
await jj.Git.PushAsync(remote: "origin", bookmark: "main");
// Roll back if anything looked wrong
await jj.Op.UndoAsync();To clone a repo when no local working directory exists yet, use the static factory:
var jj = await Jj.GitCloneAsync(
ProcessRunner.Default,
"https://github.com/owner/repo.git",
"/path/to/clone");jj command |
API | Shape |
|---|---|---|
jj status |
jj.StatusAsync |
Task<ProcessResult<string>> |
jj log (raw) |
jj.LogRawAsync |
IAsyncEnumerable<string> |
jj log (typed) |
jj.LogAsync |
IAsyncEnumerable<JjLogEntry> |
jj diff |
jj.DiffAsync |
Task<ProcessResult<string>> |
jj show |
jj.ShowAsync |
Task<ProcessResult<string>> |
jj new |
jj.NewAsync |
Task<ProcessResult<string>> (throws on fail) |
jj describe |
jj.DescribeAsync |
Task<ProcessResult<string>> (throws on fail) |
jj edit |
jj.EditAsync |
Task<ProcessResult<string>> (throws on fail) |
jj abandon |
jj.AbandonAsync |
Task<ProcessResult<string>> (throws on fail) |
jj restore |
jj.RestoreAsync |
Task<ProcessResult<string>> (throws on fail) |
jj squash |
jj.SquashAsync |
Task<ProcessResult<string>> (throws on fail) |
jj rebase |
jj.RebaseAsync |
Task<ProcessResult<string>> (throws on fail) |
jj git fetch |
jj.Git.FetchAsync |
Task<ProcessResult<string>> (throws on fail) |
jj git push |
jj.Git.PushAsync |
Task<ProcessResult<string>> (throws on fail) |
jj git clone |
Jj.GitCloneAsync (static) |
Task<Jj> (throws on fail) |
jj op log (raw) |
jj.Op.LogRawAsync |
IAsyncEnumerable<string> |
jj op log (typed) |
jj.Op.LogAsync |
IAsyncEnumerable<JjOperationEntry> |
jj op undo |
jj.Op.UndoAsync |
Task<ProcessResult<string>> (throws on fail) |
jj op restore |
jj.Op.RestoreAsync |
Task<ProcessResult<string>> (throws on fail) |
jj bookmark list (raw) |
jj.Bookmark.ListRawAsync |
IAsyncEnumerable<string> |
jj bookmark list (typed) |
jj.Bookmark.ListAsync |
IAsyncEnumerable<JjBookmarkEntry> |
jj bookmark set |
jj.Bookmark.SetAsync |
Task<ProcessResult<string>> (throws on fail) |
jj bookmark delete |
jj.Bookmark.DeleteAsync |
Task<ProcessResult<string>> (throws on fail) |
Every method takes a CancellationToken and runs jj --no-pager under the hood. Typed
overloads use a private unit-separator template and never parse human-readable jj output.
This project is licensed under the MIT License.