Logo VcsToolkit

Examples

These examples use the public APIs implemented by the corresponding projects (and, after the first release, shipped in their NuGet packages). The toolkit runs the installed VCS and forge CLIs, so use it only with repositories and credentials you are allowed to modify.

Repository state, merge probes, and worktrees

Repo.Open detects Git or Jujutsu. A merge probe rolls itself back before returning, and the worktree call creates a new branch from the requested base revision.

open VcsToolkit.Core

let inspectAndCreateWorktree repoDir =
    task {
        match Repo.Open repoDir with
        | Error error -> return Error error
        | Ok repo ->
            match! repo.Snapshot() with
            | Error error -> return Error error
            | Ok snapshot ->
                printfn "Head: %A; branch: %A" snapshot.Head snapshot.Branch

                let! workingCopyDiff = repo.Diff()
                printfn "Working-copy unified diff: %A" workingCopyDiff

                let! merge = repo.TryMerge "origin/main"
                printfn "Merge probe: %A" merge

                let! existing = repo.ListWorktrees()
                printfn "Attached worktrees: %A" existing

                return! repo.CreateWorktree("../docs-worktree", "docs-example", "main")
    }

Pull request lifecycle

Construct the facade for the forge that hosts the repository. This GitHub example uses the ambient gh login; use Forge.GitLab or Forge.Gitea for those CLIs instead. PrCreate returns the CLI's success output — a URL on GitHub/GitLab — not the PR number, so query by the exact source branch to get the ForgePr.Number. Do not take the first item from an unfiltered PR list: it may belong to somebody else's branch.

open VcsToolkit.Forge

let createAndMergePullRequest repoDir sourceBranch =
    task {
        let forge = Forge.GitHub repoDir
        let spec =
            PrCreate.Create("Document examples", "Adds a public API cookbook.").WithSource(sourceBranch)

        let! created = forge.PrCreate spec

        match created with
        | Error error -> return Error error
        | Ok url ->
            printfn "Opened pull request: %s" url

            match! forge.PrForBranch sourceBranch with
            | Error error -> return Error error
            | Ok prs ->
                match prs |> List.tryFind (fun pr -> pr.State = ForgePrState.Open) with
                | None -> return Error(ForgeError.InvalidInput "No open pull request found for the source branch")
                | Some pr ->
                    let! detail = forge.PrView pr.Number
                    printfn "Pull request: %A" detail
                    let! merged = forge.PrMerge(pr.Number, PrMerge.Squash)
                    return merged |> Result.map ignore
    }

Watching repository changes

VcsToolkit.Watch ships two monitors over the same event model. Pick by how the repository is reached, not by preference:

RepoWatcher

RepoPoller

Driven by

FileSystemWatcher on the .git/.jj state dir (optionally the working tree)

a plain interval timer

Latency

sub-second (Debounce, default 250 ms)

bounded below by Interval (default 2 s)

Cost while idle

none — no query until the filesystem moves

one git/jj re-query per tick, change or not

Where it works

local disks with a working OS watch

anywhere a subprocess runs

Known-bad ground

network shares (SMB/NFS), docker/podman volume mounts, across the WSL/host boundary — the OS watch is unreliable or silently absent; a removed-and-recreated state dir invalidates the watch and is only reported (WatcherStats.WatchErrors), never recovered from

— no OS-watch failure mode at all

Prefer RepoWatcher for a prompt, status bar, or TUI on a local checkout; reach for RepoPoller when the repository lives on a network share or inside a container/VM mount, or when you would otherwise have to hand-roll a timer around RepoDiff.diff. Both emit the same RepoEvents for the same series of repository mutations, and both are consumed identically — Recv() / ReadAll() / Dispose.

Build a watcher before entering the receive loop. Recv returns None after normal disposal; a terminal re-query failure is surfaced as a ChannelClosedException whose inner exception is WatcherTerminated.

open System.Threading.Channels
open VcsToolkit.Core
open VcsToolkit.Watch

let watch repoDir =
    task {
        match Repo.Open repoDir with
        | Error error -> eprintfn "Cannot open repository: %A" error
        | Ok repo ->
            match! RepoWatcher.Builder(repo).WorkingTree(true).Build() with
            | Error error -> eprintfn "Cannot start watcher: %A" error
            | Ok watcher ->
                use watcher = watcher
                try
                    let mutable running = true
                    while running do
                        match! watcher.Recv() with
                        | Some change -> printfn "Events: %A" change.Events
                        | None -> running <- false
                with
                | :? ChannelClosedException as closed ->
                    match closed.InnerException with
                    | :? WatcherTerminated as terminated ->
                        let (WatcherTerminated error) = terminated
                        eprintfn "Watcher stopped: %A" error
                    | _ -> return raise closed
    }

Swapping in the poller is a one-line change at the build site — the receive loop is unchanged, including the terminal-failure handling (RepoPoller closes its channel with the same WatcherTerminated), and ReadAll is available on both. Only the knobs differ: an Interval instead of Debounce/MaxWait, and no working-tree switch, since a poller re-queries the whole state either way.

open VcsToolkit.Watch

let poll repoDir =
    task {
        match Repo.Open repoDir with
        | Error error -> eprintfn "Cannot open repository: %A" error
        | Ok repo ->
            // On a network share or a container volume mount, where an OS filesystem watch is
            // unreliable: re-query every second instead of waiting for a notification.
            match! RepoPoller.Builder(repo).Interval(TimeSpan.FromSeconds 1.0).Build() with
            | Error error -> eprintfn "Cannot start poller: %A" error
            | Ok poller ->
                use poller = poller
                let mutable running = true

                while running do
                    match! poller.Recv() with
                    | Some change -> printfn "Events: %A" change.Events
                    | None -> running <- false
    }

ReadAll(?cancellationToken) is the IAsyncEnumerable form of the same stream on both monitors — natural from C# (await foreach, honouring both the argument token and WithCancellation). From F#, note that FSharp.Core's task { } cannot for … in an IAsyncEnumerable (that needs TaskSeq), so a Recv loop like the one above is usually the simpler F# consumer.

Resolving Git conflict markers

Parse a conflicted text file, choose a side for every region, and render the original segments when you need a byte-exact round trip instead.

open VcsToolkit.Git

let resolveOurs content =
    match Conflict.parseConflicts content with
    | Error error -> Error error
    | Ok segments ->
        let original = Conflict.render segments
        printfn "Original conflict text: %s" original
        Conflict.resolve segments ResolutionSide.Ours

Supplying credentials through ICredentialProvider

EnvToken reads the secret only when an operation runs. The GitHub client injects it as GH_TOKEN, never as a command-line argument; an unset variable falls back to ambient gh auth.

open VcsToolkit.CliSupport
open VcsToolkit.GitHub

let listPullRequests repoDir =
    task {
        let provider: ICredentialProvider = EnvToken("GITHUB_TOKEN") :> ICredentialProvider
        let github = GitHub.Create().WithCredentials provider
        return! github.PrList repoDir
    }
val inspectAndCreateWorktree: repoDir: 'a -> System.Threading.Tasks.Task<Result<'b,'c>>
val repoDir: 'a
val task: TaskBuilder
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val error: 'c
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val repo: obj
val snapshot: obj
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val workingCopyDiff: obj
val merge: obj
val existing: obj
val createAndMergePullRequest: repoDir: 'a -> sourceBranch: 'b -> System.Threading.Tasks.Task<Result<unit,'c>>
val sourceBranch: 'b
val forge: obj
val spec: obj
val created: Result<string,'c>
val url: string
val prs: obj list
Multiple items
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
val tryFind: predicate: ('T -> bool) -> list: 'T list -> 'T option
val pr: obj
union case Option.None: Option<'T>
union case Option.Some: Value: 'T -> Option<'T>
val detail: obj
val merged: Result<obj,'c>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val map: mapping: ('T -> 'U) -> result: Result<'T,'TError> -> Result<'U,'TError>
val ignore: value: 'T -> unit
namespace System
namespace System.Threading
namespace System.Threading.Channels
val watch: repoDir: 'a -> System.Threading.Tasks.Task<unit>
val error: obj
val eprintfn: format: Printf.TextWriterFormat<'T> -> 'T
val watcher: System.IAsyncDisposable
val mutable running: bool
val change: obj
Multiple items
type ChannelClosedException = inherit InvalidOperationException new: unit -> unit + 3 overloads
<summary>Exception thrown when a channel is used after it's been closed.</summary>

--------------------
ChannelClosedException() : ChannelClosedException
ChannelClosedException(innerException: exn) : ChannelClosedException
ChannelClosedException(message: string) : ChannelClosedException
ChannelClosedException(message: string, innerException: exn) : ChannelClosedException
val closed: ChannelClosedException
property System.Exception.InnerException: exn with get
val terminated: exn
val raise: exn: System.Exception -> 'T
val poll: repoDir: 'a -> System.Threading.Tasks.Task<unit>
val poller: System.IAsyncDisposable
val resolveOurs: content: 'a -> Result<'b,'c>
val content: 'a
val segments: obj
val original: string
val listPullRequests: repoDir: 'a -> System.Threading.Tasks.Task<'b>
val provider: obj
val github: obj

Type something to start searching.