lsremote

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

go-ls-remote

Go Reference Go Report Card

Give a man a fish and you feed him for a day, give him an LLM and he will torture it into writing a spec-compliant Go port of git ls-remote.

A Go library for the discovery half of Git's wire protocols, i.e. the surface you hit when you run git ls-remote. Self-contained: no shelling out to a git binary, no cgo. Meant for tooling that needs to know what's on a remote (branch heads, tags, default branch) without cloning the repository or pulling in a full Git library like go-git, git2go, or libgit2.

This library:

  • Sticks to discovery. Ref advertisements with prefix filtering, peeled annotated tags, symref resolution, unborn-HEAD reporting, capability discovery, default-branch resolution, a reachability probe, and the v2 object-info command. No clone, fetch, push, or working-tree code lives anywhere in the module.
  • Speaks all three wire-protocol versions (v0, v1, v2) over all four transports canonical Git supports: HTTP(S), both smart and legacy dumb HTTP; SSH; the anonymous git:// daemon; and local file:// repositories. Version negotiation matches canonical Git: a caller can pin a version, but by default the library asks for v2 and falls back to v1 or v0 when the server only speaks an earlier protocol.
  • Pulls only the Go standard library for HTTP(S). SSH adds golang.org/x/crypto/ssh; the local-file transport adds golang.org/x/exp/mmap. Each dependency is opt-in and only pulled in when the corresponding transport is imported.
  • Queries an object's size on the remote, without downloading the object, via the v2 object-info command on a long-lived Session that spreads the handshake cost across multiple discovery commands. Refs stream as an iter.Seq2[Ref, error], and an early break drains the response so the session stays usable afterwards.
  • Serves file:// URLs by running its own in-process upload-pack loop against the on-disk repository, which means the library has to bundle read-only parsers for every format that loop needs to consult: loose refs, packed-refs, and reftable; loose objects, pack v1/v2, and the multi-pack-index (including walking the objects/info/alternates chain). Both sha1 and sha256 repositories work, via a typed-per-algorithm hash abstraction.
  • Funnels every transport-level failure (HTTP status, SSH fatal, file not-found, etc.) through a single ProtocolError to public sentinels (ErrNotFound, ErrAuthRequired, ErrAuthFailed, ErrUnsupportedProtocol, ErrServerRefused, ErrNoDefaultBranch, ErrSessionDead), so callers can match with errors.Is and not worry about transport-specific types.
  • Exposes a Tracer interface for per-packet and per-event observability. Every emission site is gated by an explicit if tracer != nil check, so passing no tracer costs one nil comparison per event. No allocations, no formatting.
  • Does not parse ~/.gitconfig or ~/.ssh/config, and does not speak the git credential helper protocol. Built-in helpers cover Basic, Bearer, .netrc, SSH agent, and SSH key authentication. Anything beyond that is on the caller.

For more on how to actually use this, see the Go Reference.

Compatibility with canonical Git

Canonical Git is the single source of truth for both wire behaviour and on-disk format handling. Every protocol-sensitive decision was made by reading the corresponding builtin/, connect.c, or pkt-line.c source, or the relevant Documentation/gitprotocol-*.adoc paragraph, before the test or implementation got written. Load-bearing decisions carry a code citation pinned to a specific Git version (currently v2.54.0). Where Git's documented behaviour and its source-code behaviour disagree, we follow the source and leave a comment explaining the divergence.

Test fixtures are real, not synthetic. The canonical corpus under testdata/canonical/ is captured by running an actual git upload-pack against a pinned set of repositories and committing the resulting bytes. The in-process server emulator and the wire codec are then validated by comparing their output to those captured bytes, byte for byte. A small mask layer names every region the two implementations are allowed to differ in, and rejects new entries unless the divergence is itself protocol-compliant.

Three categories of divergence are currently documented. First, content that legitimately varies even when the framing is identical: the agent capability, where canonical and the emulator advertise different version strings, the same way any two git versions would. Second, capabilities canonical advertises by default that this library has chosen not to implement: fetch and server-option, both excluded by the discovery-only scope. Third, capabilities the emulator advertises unconditionally that canonical only emits under feature.experimental, currently object-info, fully implemented here and exposed as a first-class API. The framing itself (pkt-line lengths, flush placement, capability-list ordering) is never masked: a divergence there is a bug, not a difference to paper over.

The same comparison is then run against every transport. An in-process integration suite spins up HTTP(S), SSH, git://, and file:// harnesses against a shared fixture matrix and asserts that every discovery command returns identical results across all four wires. A regression in the wire codec, the in-process server, or URL parsing then surfaces as a divergence between transports rather than as silent drift. A separate nightly suite hits public repositories on GitHub, GitLab, Codeberg, Bitbucket, and Gitea over unauthenticated HTTPS, catching production-server behaviour drift that in-process tests can't.

Background

This library was built primarily as an experiment with LLM- and agent-driven software development on a non-trivial, protocol-level codebase. A secondary goal is to find patterns (in architecture, scope discipline, error handling, and observability) that might be useful as work on go-git continues.

Contributing

This library is feature-complete for its stated scope, and new features won't be accepted. Contributions that improve performance, or that close gaps in compatibility with canonical Git (builtin/ls-remote.c, connect.c, pkt-line.c, and the gitprotocol-*.adoc documentation), are welcome.

Please open an issue describing the problem before sending a pull request, so we can agree on the approach first. Every commit needs a Signed-off-by: trailer attesting to the Developer Certificate of Origin (git commit -s).

Documentation

Overview

Package lsremote implements the discovery-time half of Git's wire protocols — the surface a caller exercises when they run `git ls-remote` — as a self-contained Go library with no shell-out to a `git` binary.

The package speaks all three protocol versions (v0, v1, v2) over the transports Git itself supports: HTTP(S), SSH, the anonymous `git://` daemon, and local `file://` repositories. Version negotiation matches canonical Git: a caller may pin a preferred version via the Option helpers, but absent an explicit preference the library asks for v2 and gracefully falls back to v0 when the server only speaks the original protocol.

The library exposes two layers. One-shot helpers — see Refs, ListRefs, and ObjectInfos — open a connection, run a single discovery command, and close it; they fit the common case where the caller only wants a ref list or a few object sizes. The session layer — see Dial and Session — keeps a connection open across multiple v2 commands so callers can issue Session.Refs followed by Session.ObjectInfo without re-handshaking.

Primary types

Ref models a single entry in a discovery response: the ref name, its object id, an optional peeled object id for annotated tags, and an optional symref target when the entry is a symbolic reference.

Capabilities records what the remote advertised during the handshake: the negotiated ProtocolVersion, the server agent string, the ObjectFormat in use (`sha1` or `sha256`), and the per-command argument lists a v2 server claims to accept.

ObjectInfo is the response shape for the v2 `object-info` command, pairing an object id with metadata such as its `Size`.

Symref names a single `HEAD → refs/heads/...` style mapping as advertised by v0/v1 servers in their capability list. v2 servers surface the same information inline on each Ref via [Ref.Symref] instead, so v2 Capabilities leaves [Capabilities.Symrefs] empty.

The ProtocolVersion identifier is a Go type alias of transport.ProtocolVersion; the two are interchangeable without conversion. The constants ProtocolV0, ProtocolV1, and ProtocolV2 resolve to the same package-level constants the `transport` package defines.

Example (DefaultBranch)

Example_defaultBranch resolves the canonical name of a remote repository's default branch — the target of HEAD — without cloning or hitting the filesystem. The returned name is the full ref path (`refs/heads/main`, `refs/heads/master`, ...), matching the form a caller would write into a `git fetch` invocation.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	branch, err := lsremote.DefaultBranch(context.Background(),
		"https://github.com/octocat/Hello-World.git")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(branch)
}
Example (HandleErrors)

Example_handleErrors illustrates the package's sentinel error model. Every transport-level failure flows through lsremote.ProtocolError and bridges to one of the public sentinels via errors.Is, so callers never have to branch on transport-specific error types. Use errors.As to recover diagnostic fields such as the HTTP status code or a server-supplied excerpt.

The example dials a non-existent local path so the output is deterministic; the same error-handling shape applies to every transport.

package main

import (
	"context"
	"errors"
	"fmt"

	lsremote "github.com/hiddeco/go-ls-remote"
	"github.com/hiddeco/go-ls-remote/transport"

	filet "github.com/hiddeco/go-ls-remote/transport/file"
)

func main() {
	// The package default registers HTTPS/HTTP only; opt into the file
	// transport explicitly. See [lsremote.WithTransports] for the full
	// composition pattern.
	reg := transport.NewRegistry(filet.New())

	_, err := lsremote.Dial(context.Background(),
		"file:///does/not/exist",
		lsremote.WithTransports(reg))

	var perr *lsremote.ProtocolError
	switch {
	case errors.Is(err, lsremote.ErrNotFound):
		fmt.Println("not found")
	case errors.Is(err, lsremote.ErrAuthRequired):
		fmt.Println("auth required")
	case errors.Is(err, lsremote.ErrAuthFailed):
		fmt.Println("auth failed")
	case errors.As(err, &perr):
		fmt.Printf("protocol error at %s: %v\n", perr.Op, perr.Err)
	case err != nil:
		fmt.Printf("other error: %v\n", err)
	}
}
Output:
not found
Example (PollLatestCommit)

Example_pollLatestCommit retrieves the current commit hash of a specific branch — the building block for "is there anything new to deploy?" polling, image-automation triggers, and similar workflows. The v2 wire applies the prefix filter server-side, so the request is cheap even on remotes with millions of refs.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	ctx := context.Background()
	refs, err := lsremote.ListRefs(ctx,
		"https://github.com/octocat/Hello-World.git",
		lsremote.RefsRequest{Prefixes: []string{"refs/heads/master"}})
	if err != nil {
		log.Fatal(err)
	}
	for _, ref := range refs {
		fmt.Printf("%s\t%s\n", ref.Hash, ref.Name)
	}
}

Index

Examples

Constants

View Source
const (
	ProtocolV0 = transport.ProtocolV0
	ProtocolV1 = transport.ProtocolV1
	ProtocolV2 = transport.ProtocolV2
)

Re-exported ProtocolVersion constants. Because the type is an alias, `lsremote.ProtocolV2 == transport.ProtocolV2` and the two names refer to the same constant value.

Variables

View Source
var (
	// ErrNotFound signals the remote does not host the requested
	// repository. HTTP transports surface a `404`, SSH transports the
	// `fatal: '<path>' does not appear to be a git repository` error
	// line, and `file://` transports a missing-directory check.
	ErrNotFound = errors.New("lsremote: repository not found")

	// ErrAuthRequired signals the server demanded authentication and
	// none was offered (or the caller's credential resolver yielded
	// nothing). Callers are expected to plug in credentials and retry.
	ErrAuthRequired = errors.New("lsremote: authentication required")

	// ErrAuthFailed signals the server rejected the supplied
	// credentials. The library does not retry on its own; the caller
	// should refresh credentials before another attempt.
	ErrAuthFailed = errors.New("lsremote: authentication failed")

	// ErrUnsupportedProtocol signals the remote cannot honour the
	// requested protocol or operation — for example a dumb-HTTP
	// server when the caller asked for a v2 command, or an
	// `object-info` request against a v2 server that did not
	// advertise the command.
	ErrUnsupportedProtocol = errors.New("lsremote: protocol/operation not supported by server")

	// ErrServerRefused signals the server returned an explicit
	// `ERR <message>` pkt-line or otherwise refused the request after
	// the connection was established. The originating message is
	// preserved on [ProtocolError.Server] when available.
	ErrServerRefused = errors.New("lsremote: server refused")

	// ErrNoDefaultBranch signals that the remote repository is reachable
	// but HEAD has no symbolic target — it is either detached or the
	// server omitted the symref mapping from its advertisement. Returned
	// by [DefaultBranch] when no HEAD symref can be resolved. The
	// surrounding [ProtocolError] carries `Op == "ls-refs"` on a v2
	// server (the mapping is sought via the `ls-refs` command) and
	// `Op == "advertisement"` on a v0/v1 server (the mapping is sought
	// in the capability advertisement). Use [errors.Is] to distinguish
	// this from [ErrNotFound], which means the repository itself is
	// absent.
	ErrNoDefaultBranch = errors.New("lsremote: remote has no default branch")

	// ErrSessionDead signals that the [Session] is no longer usable
	// because a previous command encountered a mid-stream wire error
	// that left the underlying byte stream in an undefined position.
	// The Session marks itself dead, closes its [transport.Conn]
	// eagerly, and rejects subsequent commands with this sentinel
	// wrapped in a [*ProtocolError]. The original cause is preserved
	// on the `errors.Is` chain so callers can match against both.
	//
	// Returned only by [Session.Refs], [Session.ListRefs], and
	// [Session.ObjectInfo] — [Session.Capabilities] continues to work
	// on a dead session because it reads cached state.
	// [Session.Close] remains idempotent.
	ErrSessionDead = errors.New("lsremote: session is no longer usable")
)

Sentinel errors returned by the discovery surface. Each carries the `lsremote:` prefix so a stray log line is grep-friendly, and each is matched with errors.Is — typically against a ProtocolError whose [ProtocolError.Err] field wraps the sentinel directly or through one or more transport-layer wrappers.

Transport-specific sentinels (e.g. `transport/http.ErrNotFound`) are deliberately not re-exported: the errors.Is chain walks through ProtocolError.Unwrap, so a caller writes `errors.Is(err, lsremote.ErrNotFound)` without having to know which transport produced the failure.

Functions

func DefaultBranch

func DefaultBranch(ctx context.Context, rawURL string, opts ...Option) (string, error)

DefaultBranch returns the canonical name of the branch HEAD points at on the remote — `refs/heads/main`, `refs/heads/master`, etc.

On v2 the helper issues `ls-refs` with `ref-prefix HEAD`, `symrefs`, and `unborn`, then returns the [Ref.Symref] target attached to the `HEAD` entry. The `unborn` argument mirrors canonical Git's discovery client (connect.c:591-592). The wire layer silently ignores it when the server has not advertised `ls-refs=unborn`, so passing it here is always safe — older servers simply fall through to the v0/v1-style capability-list scan below. With `unborn`, a branch HEAD with no commits yet still surfaces as an unborn `HEAD` entry whose `symref-target:` carries the branch name; without it the server (ls-refs.c:135-136) would suppress HEAD entirely and the helper would fall through to ErrNoDefaultBranch. When the v2 server does not honour `symrefs` (or HEAD is detached and so carries no symref target) the helper falls back to the v0/v1-style capability scan over [Capabilities.Symrefs], because some servers surface the mapping there even on a v2 handshake.

On v0/v1 the wire has no `ls-refs` equivalent and the symref information rides on the capability list; the helper scans [Capabilities.Symrefs] for an entry named `HEAD` and returns its target.

When no symref target can be resolved DefaultBranch returns a *ProtocolError whose `Err` chains to ErrNoDefaultBranch. The surrounding [ProtocolError.Op] is `"ls-refs"` on a v2 server (the mapping is sought via the `ls-refs` command exchange) and `"advertisement"` on a v0/v1 server (the mapping is sought in the capability advertisement). Use `errors.Is(err, ErrNoDefaultBranch)` to detect the "repository present but HEAD has no symbolic target" condition (a detached HEAD on v2, or a v0/v1 server whose advertisement omits a `symref=HEAD:...` capability). A dial failure whose chain matches ErrNotFound means the repository itself is absent — the two sentinels are mutually exclusive.

func Exists

func Exists(ctx context.Context, rawURL string, opts ...Option) (bool, error)

Exists reports whether a repository is reachable at rawURL.

A successful Dial yields `(true, nil)` and the helper closes the resulting Session before returning. A Dial failure whose error chain matches ErrNotFound collapses to `(false, nil)` so a caller can distinguish "missing repository" from "transport blew up" without inspecting the error. Any other error propagates verbatim as `(false, err)`.

Example

ExampleExists reports whether a remote repository is reachable. A missing-repository condition collapses to (false, nil), so callers can distinguish "not there" from "transport failure" without inspecting the error. Authentication failures, DNS errors, and other transport-level problems still propagate as the error return.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	ok, err := lsremote.Exists(context.Background(),
		"https://github.com/octocat/Hello-World.git")
	if err != nil {
		log.Fatal(err)
	}
	if ok {
		fmt.Println("found")
	} else {
		fmt.Println("missing")
	}
}

func Heads

func Heads(ctx context.Context, rawURL string,
	opts ...Option,
) (iter.Seq2[Ref, error], error)

Heads is a shorthand for Refs restricted to `refs/heads/`.

func Refs

func Refs(ctx context.Context, rawURL string, args RefsRequest,
	opts ...Option,
) (iter.Seq2[Ref, error], error)

Refs is the one-shot form of Session.Refs. It dials the remote at rawURL, requests an `ls-refs` stream (or filters the v0/v1 advertisement-time cache, per Session.Refs's version split), and arranges for the underlying Session to close when the returned iterator is exhausted or abandoned.

The Session lifetime is tied to the iterator: the wrapper's deferred Session.Close runs when the inner range-over-func returns, whether via exhaustion of the source stream or via the caller's `break` (the latter surfaces as `yield` returning false). Callers should not hold the iterator across goroutines or store it for later draining.

On a dial-time or `ls-refs`-time failure Refs returns `(nil, err)` and leaves no Session open.

func Tags

func Tags(ctx context.Context, rawURL string,
	opts ...Option,
) (iter.Seq2[Ref, error], error)

Tags is a shorthand for Refs restricted to `refs/tags/` with `Peel` set so annotated tags carry their peeled commit id on [Ref.Peeled]. Tags do not carry symref targets, so [Ref.Symref] is always empty on every ref yielded by this iterator.

Example

ExampleTags streams a remote repository's tags through an iterator. lsremote.Ref.Peeled carries the commit OID an annotated tag points to; lightweight tags leave lsremote.Ref.Peeled empty and lsremote.Ref.Hash already points directly at the commit.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	seq, err := lsremote.Tags(context.Background(),
		"https://github.com/octocat/Hello-World.git")
	if err != nil {
		log.Fatal(err)
	}
	for ref, err := range seq {
		if err != nil {
			log.Fatal(err)
		}
		commit := ref.Hash
		if ref.Peeled != "" {
			commit = ref.Peeled
		}
		fmt.Printf("%s\t%s\n", commit, ref.Name)
	}
}

Types

type Capabilities

type Capabilities struct {
	// Version is the negotiated protocol version.
	Version ProtocolVersion

	// Agent is the server's `agent=` advertisement, or the empty
	// string when none was sent.
	Agent string

	// ObjectFormat is the negotiated repository object format.
	// v0/v1 handshakes that omit `object-format` normalise to
	// [ObjectFormatSHA1]; v2 handshakes that omit it produce
	// the empty string (a server-side protocol violation).
	ObjectFormat ObjectFormat

	// Commands is the curated intersection of v2 commands this
	// library can issue and the server advertised. Empty for v0/v1
	// handshakes. [Capabilities.Raw] carries the verbatim wire-level
	// capability set for callers that need it.
	Commands []string

	// LSRefsArgs lists the per-command arguments the server
	// accepts on `ls-refs`.
	LSRefsArgs []string

	// ObjectInfoArgs lists the per-command arguments the server
	// accepts on `object-info`.
	ObjectInfoArgs []string

	// Symrefs lists v0/v1 capability-level symref advertisements.
	// Empty for v2 handshakes.
	Symrefs []Symref

	// Raw is every advertised capability, verbatim, keyed by
	// capability name. The value slice preserves both repeated
	// advertisements and original order.
	Raw map[string][]string
}

Capabilities records what a remote advertised during the discovery handshake: the negotiated ProtocolVersion, the server agent string, the repository's ObjectFormat, and (for v2) the commands and per-command argument lists the server claims to support.

Field semantics

  • [Capabilities.Version] is the protocol version actually negotiated, not the one the caller requested.
  • [Capabilities.Agent] is the `agent=` capability value the server advertised, or the empty string when the server did not send one.
  • [Capabilities.ObjectFormat] is the negotiated repository object format. When a v0 or v1 server omits `object-format`, the field is normalised to ObjectFormatSHA1 — omitting the capability on those protocol versions always implies SHA-1. When a v2 server omits `object-format`, the field is the empty string, signalling a protocol violation rather than a default. Unknown values advertised explicitly are preserved verbatim so callers can detect future formats.
  • [Capabilities.Commands] is the curated set of v2 commands that both the server advertised and this library knows how to issue through Session methods. It is empty for v0/v1 handshakes, where command-style negotiation does not exist. Callers who want the verbatim wire-level capability set — including future v2 commands this library does not yet implement — can read [Capabilities.Raw] directly.
  • [Capabilities.LSRefsArgs] and [Capabilities.ObjectInfoArgs] list the per-command arguments the server claims to accept for `ls-refs` and `object-info` respectively. The `fetch` command's argument list is not exposed as a typed field — the library never issues `fetch`, and callers that want the advertised arg list can read `Capabilities.Raw["fetch"]` verbatim.
  • [Capabilities.Symrefs] lists `HEAD → refs/heads/...` style mappings advertised in the v0/v1 capability list. v2 servers surface symrefs inline on each Ref instead, so this slice is empty for v2 handshakes.
  • [Capabilities.Raw] holds every capability the server advertised, keyed by capability name with each value's argument(s) preserved verbatim. It is a `map[string][]string` because the same capability name can appear multiple times in a single advertisement — `symref=HEAD:refs/heads/main` and `symref=refs/remotes/origin/HEAD:refs/heads/main`, for example — and the slice keeps every occurrence in advertised order. Capabilities advertised without a value (a bare `multi_ack` token) appear as a one-element slice containing the empty string.

Caller contract

Callers must treat a returned Capabilities and any slice or map it contains as read-only. The library deep-copies the value before handing it back so mutation by the caller cannot corrupt internal state, but mutating the returned value is undefined and reserved for the library's own use.

Capabilities has no methods; callers read the exported fields directly.

type ObjectFormat

type ObjectFormat string

ObjectFormat identifies the cryptographic hash function a remote repository uses to name objects. The string values are the literal tokens Git puts on the wire (`sha1` for the historical default and `sha256` for the newer object format), so callers can compare against the constants without case folding.

const (
	// ObjectFormatSHA1 is the historical Git object format whose
	// object ids are 40-character hexadecimal SHA-1 digests.
	ObjectFormatSHA1 ObjectFormat = "sha1"

	// ObjectFormatSHA256 is the newer Git object format whose
	// object ids are 64-character hexadecimal SHA-256 digests.
	ObjectFormatSHA256 ObjectFormat = "sha256"
)

type ObjectInfo

type ObjectInfo struct {
	// Hash is the queried object id in lower-case hexadecimal.
	Hash string

	// Size is the object's payload size in bytes, or `-1` when
	// the size was not requested or not returned by the server.
	Size int64
}

ObjectInfo carries the per-object metadata returned by the v2 `object-info` command.

[ObjectInfo.Hash] is the queried object id in lower-case hexadecimal. [ObjectInfo.Size] is the object's payload size in bytes when the caller requested it and the server returned a value, or `-1` when the size was not requested by the caller or not returned by the server. A real on-disk object can legitimately have a size of zero (an empty blob), so the negative sentinel is the only unambiguous "absent" marker.

ObjectInfo has no methods; callers read the exported fields directly.

func ObjectInfos

func ObjectInfos(ctx context.Context, rawURL string, oids []string,
	args ObjectInfoRequest, opts ...Option,
) ([]ObjectInfo, error)

ObjectInfos is the one-shot form of Session.ObjectInfo. It dials, issues an `object-info` command, and closes the underlying Session before returning. The plural name avoids the Go-level collision with the ObjectInfo response type while keeping the relationship with Session.ObjectInfo obvious at the call site.

`object-info` is a v2-only command. A server that negotiates v0 or v1 (or a v2 server that did not advertise `object-info`) produces a *ProtocolError whose chain matches ErrUnsupportedProtocol, per Session.ObjectInfo.

type ObjectInfoRequest

type ObjectInfoRequest struct {
	// Size asks the server to return the payload size in bytes for
	// each queried object id. Maps to the `size` argument of v2
	// `object-info` and surfaces on [ObjectInfo.Size].
	Size bool
}

ObjectInfoRequest collects the per-command arguments for an `object-info` invocation — the inputs a caller passes to Session.ObjectInfo and the corresponding top-level helper.

ObjectInfoRequest is a plain data carrier with no methods. The zero value is valid and queries the listed object ids without asking for any per-object attributes.

Field semantics

  • [ObjectInfoRequest.Size] asks the server to return the payload size in bytes for each queried object id. It maps to the `size` argument of the v2 `object-info` command and surfaces on [ObjectInfo.Size]. `object-info` is a v2-only command; on v0/v1 handshakes the library never issues it and this flag is unused.

See gitprotocol-v2.adoc §"object-info" for the v2 wire contract.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures a Dial. Compose Options from the `With*` constructors exported by this package; the interface is intentionally sealed via the unexported `applyDial` method so the option set cannot grow outside this package.

func WithProtocol

func WithProtocol(v ProtocolVersion) Option

WithProtocol pins protocol negotiation to a specific wire version. Omit the option to auto-negotiate (prefer v2, fall back to v0).

There is no `Auto` sentinel: absence of WithProtocol is the auto signal. The constructor captures v by value into a fresh pointer so later mutation of the caller's variable cannot affect the stored preference.

Example

ExampleWithProtocol pins protocol-version negotiation. Auto- negotiation (the default) prefers v2 and falls back to v0; pin v0 only when the caller knows the remote does not understand v2, or to reproduce a historical interaction.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	refs, err := lsremote.ListRefs(context.Background(),
		"https://example.com/git/legacy.git",
		lsremote.RefsRequest{},
		lsremote.WithProtocol(lsremote.ProtocolV0))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(refs), "refs")
}

func WithTracer

func WithTracer(t trace.Tracer) Option

WithTracer installs t as the trace.Tracer for protocol observability. A nil tracer disables tracing — the default — and every emission site in the library short-circuits on an explicit nil check.

Example

ExampleWithTracer captures pkt-line, HTTP, and command lifecycle events through a trace.Tracer. trace.NewWriterTracer dumps a human-readable summary to an io.Writer, comparable to canonical Git's `GIT_TRACE_PACKET=` output; implement trace.Tracer directly for structured or machine-readable consumption.

package main

import (
	"context"
	"log"
	"os"
	"time"

	lsremote "github.com/hiddeco/go-ls-remote"
	"github.com/hiddeco/go-ls-remote/trace"
)

func main() {
	tracer := trace.NewWriterTracer(os.Stderr)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	_, err := lsremote.ListRefs(ctx,
		"https://github.com/octocat/Hello-World.git",
		lsremote.RefsRequest{},
		lsremote.WithTracer(tracer))
	if err != nil {
		log.Fatal(err) //nolint:gocritic // example doc pattern: log.Fatal mirrors typical caller code
	}
}

func WithTransports

func WithTransports(r *transport.Registry) Option

WithTransports replaces the default HTTP-only transport registry with r. Use it to opt into SSH, git, or file transports, or to substitute a custom HTTP client.

Passing nil is permitted and means "fall back to the package default" — Dial performs the substitution at call time, so a nil here is indistinguishable from omitting the option.

Example

ExampleWithTransports composes a registry covering schemes beyond the HTTPS-only default. SSH and `git://` are intentionally opt-in so a misconfigured caller cannot accidentally reach a non-HTTPS remote; the same pattern adds them once the per-transport options (credentials, host-key callback) have been wired in. See the `ssht` and `gitt` package examples for the per-transport setup.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
	"github.com/hiddeco/go-ls-remote/transport"

	filet "github.com/hiddeco/go-ls-remote/transport/file"

	gitt "github.com/hiddeco/go-ls-remote/transport/git"

	httpt "github.com/hiddeco/go-ls-remote/transport/http"
)

func main() {
	reg := transport.NewRegistry(
		httpt.New(),
		gitt.New(),
		filet.New(),
	)

	ctx := context.Background()
	refs, err := lsremote.ListRefs(ctx, "git://example.com/repo.git",
		lsremote.RefsRequest{},
		lsremote.WithTransports(reg))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(refs), "refs")
}

func WithUserAgent

func WithUserAgent(s string) Option

WithUserAgent overrides the User-Agent string sent on HTTP-based transports. The empty string means "use the transport's own default," which matches omitting the option entirely.

type ProtocolError

type ProtocolError struct {
	// URL is the request URL with credentials redacted via
	// [transport.RedactURL]. The redaction is reapplied by
	// [ProtocolError.Error] so the formatted output is always safe
	// to log.
	URL string

	// Op identifies the discovery step that failed. See the type
	// doc for the allowed values.
	Op string

	// Version is the negotiated [ProtocolVersion], or nil when the
	// error occurred before the handshake completed.
	Version *ProtocolVersion

	// Server is the server-supplied error message (a `ERR` pkt-line
	// payload, an HTTP body excerpt, or similar). Bounded by the
	// transport layer to approximately 1 KiB; when the original body
	// exceeded that bound the excerpt ends in `"..."` so callers can
	// detect truncation. The public surface preserves the bytes the
	// transport emitted verbatim and does not re-truncate.
	Server string

	// Err is the wrapped cause. It typically matches one of the
	// package sentinels via [errors.Is]; nil when no specific cause
	// is available.
	Err error

	// Status is the HTTP status code, or 0 when the failure
	// occurred over a non-HTTP transport or before a response was
	// received.
	Status int
}

ProtocolError carries diagnostic context for a protocol-level failure raised by the discovery surface. It wraps an underlying cause — typically one of the package-level sentinels above — and adds enough context (URL, operation, negotiated version, HTTP status, server-supplied message) for a caller to log or report the failure without having to reach into transport internals.

Matching with errors.Is

ProtocolError.Unwrap returns [ProtocolError.Err], so `errors.Is(perr, ErrNotFound)` succeeds when `perr.Err` is `ErrNotFound` or transitively wraps it via one or more `fmt.Errorf("...: %w", ...)` calls.

The Op values

[ProtocolError.Op] names the discovery step that failed:

  • `"dial"` — connection setup before the wire handshake began.
  • `"probe"` — the initial discovery request (`GET .../info/refs`, or the equivalent on non-HTTP transports) used to detect protocol version and capabilities.
  • `"advertisement"` — parsing the server's capability/ref advertisement.
  • `"ls-refs"` — the v2 `ls-refs` command exchange.
  • `"object-info"` — the v2 `object-info` command exchange.

Field invariants

[ProtocolError.URL] is always credential-redacted via transport.RedactURL before it surfaces to a caller. The formatter in ProtocolError.Error applies the redaction on every call, so a caller cannot accidentally leak a password by reading the field directly and concatenating it into a log line — though callers are nevertheless encouraged to populate the field with an already-redacted value.

[ProtocolError.Server] is bounded to approximately 1 KiB: the transport layer caps the server-supplied excerpt at 1 KiB and appends a trailing `"..."` marker when the original body was longer, so the surfaced string can be up to 1 KiB plus the three-byte ellipsis. The marker lets callers tell a short body from one that was truncated; the public surface preserves it verbatim and does not re-truncate.

Format

ProtocolError.Error returns a one-line summary of the form

lsremote: <Op>: <Err> (URL <redacted-url>) [status N] [server: <truncated>]

The bracketed bits are elided when their backing field is zero: `status N` is omitted when [ProtocolError.Status] is zero, and the `server:` section is omitted when [ProtocolError.Server] is empty. A nil [ProtocolError.Err] is printed as `<nil cause>` so the formatter never panics.

func (*ProtocolError) Error

func (e *ProtocolError) Error() string

Error formats e as a single line of the form documented on ProtocolError. The URL is credential-redacted via transport.RedactURL on every call, so the returned string is always safe to log.

func (*ProtocolError) Unwrap

func (e *ProtocolError) Unwrap() error

Unwrap returns the wrapped cause so errors.Is and errors.As walk through e to the underlying sentinel or transport error.

type ProtocolVersion

type ProtocolVersion = transport.ProtocolVersion

ProtocolVersion is a Go type alias of transport.ProtocolVersion. The alias means the two are interchangeable without conversion: a `transport.ProtocolVersion` value is also a `lsremote.ProtocolVersion`, and the ProtocolV0, ProtocolV1, and ProtocolV2 constants below resolve to the same package-level constants the transport package defines.

type Ref

type Ref struct {
	// Name is the ref name, e.g. `HEAD`, `refs/heads/main`,
	// `refs/tags/v1.0.0`.
	Name string

	// Hash is the hexadecimal object id the ref points at. It is
	// 40 characters for `sha1` and 64 characters for `sha256`,
	// or the empty string for an unborn `HEAD`.
	Hash string

	// Peeled is the hexadecimal object id of the peeled commit
	// for an annotated tag, or the empty string when [Ref.Name]
	// is not a peeled annotated tag.
	Peeled string

	// Symref is the target ref name when [Ref.Name] is a symbolic
	// reference and the server disclosed the target, or the empty
	// string otherwise.
	Symref string
}

Ref names a single Git reference as it appears in the remote's discovery response.

The exported fields mirror the wire representation directly so a caller can read whatever it needs without indirection:

  • [Ref.Name] is the canonical ref name — `HEAD`, `refs/heads/main`, `refs/tags/v1.0.0`, and so on.
  • [Ref.Hash] is the hexadecimal object id the ref points at — 40 characters for `sha1` repositories and 64 for `sha256`. It is the empty string for the unborn-`HEAD` case where a server advertises `HEAD` with no object yet.
  • [Ref.Peeled] is the hexadecimal object id of the underlying commit when [Ref.Name] is an annotated tag and the server supplied a peeled value (either inline via `^{}` on the v0/v1 wire or via the v2 `peel` argument to `ls-refs`). It is empty when the ref is not a peeled annotated tag.
  • [Ref.Symref] is the target ref name when this entry is a symbolic reference and the server disclosed the target. It is empty otherwise.

Ref has no methods; callers read the exported fields directly.

func ListRefs

func ListRefs(ctx context.Context, rawURL string, args RefsRequest,
	opts ...Option,
) ([]Ref, error)

ListRefs is the one-shot form of Session.ListRefs. It dials, collects every ref into a slice, and closes the underlying Session before returning.

On a dial-time or `ls-refs`-time failure ListRefs returns `(nil, err)` and leaves no Session open.

Example

ExampleListRefs collects every advertised ref into a slice. Use this when the caller wants the slice form rather than the iterator returned by lsremote.Refs; the two share the same underlying `ls-refs` exchange on a v2 server.

lsremote.RefsRequest.Symrefs asks the server to disclose the target of symbolic references (`HEAD`, `refs/remotes/origin/HEAD`, ...); lsremote.Ref.Symref is empty for non-symbolic refs.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	refs, err := lsremote.ListRefs(context.Background(),
		"https://github.com/octocat/Hello-World.git",
		lsremote.RefsRequest{Symrefs: true})
	if err != nil {
		log.Fatal(err)
	}
	for _, ref := range refs {
		if ref.Symref != "" {
			fmt.Printf("%s -> %s\n", ref.Name, ref.Symref)
			continue
		}
		fmt.Printf("%s %s\n", ref.Hash, ref.Name)
	}
}

type RefsRequest

type RefsRequest struct {
	// Prefixes restricts the returned refs to those whose names
	// begin with one of the listed strings. Applied server-side on
	// v2 and client-side on v0/v1. An empty or nil slice means no
	// filtering.
	Prefixes []string

	// Peel asks the server to include peeled object ids for
	// annotated tags. On v2 this maps to the `peel` argument; on
	// v0/v1 peeled values ride inline on the advertisement.
	Peel bool

	// Symrefs asks the server to disclose symref targets alongside
	// each [Ref]. On v2 this maps to the `symrefs` argument and
	// surfaces on [Ref.Symref]. On v0/v1 the flag has no wire effect,
	// but the library post-fills [Ref.Symref] from
	// [Capabilities.Symrefs] when the flag is set.
	Symrefs bool

	// Unborn asks a v2 server to advertise an unborn `HEAD`. The
	// flag is honoured only when the server advertises
	// `ls-refs=unborn`. On v0/v1 the flag has no effect.
	Unborn bool
}

RefsRequest collects the per-command arguments for an `ls-refs` invocation — the inputs a caller passes to Session.Refs, Session.ListRefs, and the corresponding top-level helpers.

RefsRequest is a plain data carrier with no methods. The zero value is valid and asks the server for every ref it would normally advertise, without peeled object ids, symref targets, or an unborn `HEAD`.

Field semantics

  • [RefsRequest.Prefixes] restricts the returned refs to those whose names begin with one of the listed strings. On the v2 wire the filter is applied server-side: the library forwards each entry verbatim as a `ref-prefix <value>` argument to `ls-refs`. On the v0/v1 wire there is no equivalent capability, so the library fetches the full advertisement and applies the filter client side. Either way, an empty or nil [RefsRequest.Prefixes] means "do not filter".
  • [RefsRequest.Peel] asks the server to include the peeled object id for annotated tags. On v2 it maps to the `peel` argument to `ls-refs`; on v0/v1 the peeled value rides inline on the advertisement (`<oid> <name>^{}`) regardless of this flag, and the library keeps the flag's behaviour consistent by surfacing the value on [Ref.Peeled] only when the caller asked for it.
  • [RefsRequest.Symrefs] asks the server to disclose symref targets alongside each Ref. On v2 it maps to the `symrefs` argument to `ls-refs` and surfaces inline on [Ref.Symref]. On v0/v1 the flag has no wire effect (the symref info already rides on the capability list), but the library post-fills [Ref.Symref] from [Capabilities.Symrefs] when the flag is set, unifying the call-site experience with v2. When the flag is not set on v0/v1, [Ref.Symref] is always empty; the raw mapping remains available on [Capabilities.Symrefs] regardless.
  • [RefsRequest.Unborn] asks a v2 server to advertise an unborn `HEAD` — a `HEAD` whose target ref exists but holds no commit yet. The flag maps to the `unborn` argument to `ls-refs` and is honoured only when the server advertises `ls-refs=unborn` in its capability list. On v0/v1 the wire has no concept of an unborn `HEAD`, so the flag is ignored.

See gitprotocol-v2.adoc §"ls-refs" for the v2 wire contract.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session represents an open discovery-time connection to a remote Git repository. A Session is produced by Dial after the handshake completes; methods on a Session issue one or more discovery commands — `ls-refs`, `object-info` — against the same underlying connection.

Opaque type

Session has no exported fields. Callers obtain a `*Session` from Dial and interact with it only through its methods; the internal state — the live transport.Conn, the negotiated Capabilities, the cached advertisement-time refs (for v0/v1 only), the captured dial configuration and the redacted URL — is library-private.

Concurrency

A `*Session` is safe for concurrent use only when the underlying transport multiplexes independent commands onto independent network requests. The HTTP transport satisfies this: each v2 command is its own POST, so two goroutines can issue commands against the same HTTP-backed Session without external synchronisation. The SSH, `git://`, and `file://` transports do NOT satisfy it: they share a single bidirectional byte stream where one in-flight command must drain before the next begins. Callers using a non-HTTP transport must serialise Session method calls externally.

Lifecycle

A Session owns the underlying transport.Conn. Session.Close releases that connection; repeated calls return nil per the transport.Conn idempotent-Close contract.

func Dial

func Dial(ctx context.Context, rawURL string, opts ...Option) (*Session, error)

Dial opens a discovery-time connection to the Git repository at rawURL and returns a *Session ready to issue discovery commands.

Dial is eager: by the time it returns, the URL has been parsed, a transport has been selected, the connection has been opened, and the server's capability advertisement has been read and translated into a Capabilities value. Callers who want lazy connection setup should defer the Dial itself, not wrap it.

Options

The resolved configuration is:

  • The transport transport.Registry supplied via WithTransports, or — when omitted — an HTTP-only default registry covering the `https` and `http` schemes.
  • The trace.Tracer supplied via WithTracer, or no tracing.
  • The User-Agent string supplied via WithUserAgent, or the transport's own default.
  • The protocol version pinned by WithProtocol, or auto-negotiate when omitted. Auto-negotiation prefers v2 and falls back to v0.

Error model

Failure modes split by the discovery step that produced them.

See Option, WithTransports, WithTracer, WithUserAgent, WithProtocol, and Session for the surrounding types.

Example

ExampleDial opens one session and issues several discovery commands against it. Reusing a single session amortises the handshake — useful when a caller wants both a ref list and per-object metadata from the same remote.

A lsremote.Session is safe for concurrent use only when the underlying transport multiplexes commands onto independent requests. The HTTP transport satisfies that; SSH, `git://`, and `file://` do not — callers using a non-HTTP transport must serialise method calls externally.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	ctx := context.Background()
	session, err := lsremote.Dial(ctx,
		"https://github.com/octocat/Hello-World.git")
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	caps := session.Capabilities()
	fmt.Println("protocol     ", caps.Version)
	fmt.Println("server agent ", caps.Agent)
	fmt.Println("object format", caps.ObjectFormat)

	heads, err := session.ListRefs(ctx, lsremote.RefsRequest{
		Prefixes: []string{"refs/heads/"},
	})
	if err != nil {
		log.Fatal(err) //nolint:gocritic // example doc pattern: log.Fatal mirrors typical caller code
	}
	fmt.Println("branches:", len(heads))
}

func (*Session) Capabilities

func (s *Session) Capabilities() Capabilities

Capabilities returns a deep copy of the capability snapshot captured at Dial time. The copy is independent of the Session's internal state: mutating any slice or map on the returned value cannot affect the result of a later call.

The fields are populated per the rules documented on Capabilities itself.

Allocation

`Capabilities` allocates a fresh deep copy on every call. Callers in hot loops should cache the returned value rather than calling this method repeatedly.

func (*Session) Close

func (s *Session) Close() error

Close releases the underlying transport.Conn. The contract on transport.Conn.Close requires the implementation to be idempotent, so a second or later call after the first returns nil.

func (*Session) ListRefs

func (s *Session) ListRefs(ctx context.Context, args RefsRequest) ([]Ref, error)

ListRefs collects the refs yielded by Session.Refs into a slice. Iteration stops on the first error, which is returned alongside a nil slice; otherwise every yielded ref is appended in order.

func (*Session) ObjectInfo

func (s *Session) ObjectInfo(ctx context.Context, oids []string,
	args ObjectInfoRequest,
) ([]ObjectInfo, error)

ObjectInfo issues a v2 `object-info` command and returns the per-OID metadata the server replied with.

`object-info` is a v2-only command. A Session whose [Capabilities.Version] is not ProtocolV2, or whose v2 [Capabilities.Commands] set does not include `object-info`, returns a *ProtocolError with `Op == "object-info"` whose error chain matches ErrUnsupportedProtocol. The capability-set guard mirrors canonical Git's pre-issue check: mainstream hosts advertise v2 with only `ls-refs` and `fetch`, so a client-side short-circuit is the only way to surface a public-typed error rather than a raw transport failure.

The `oids` slice is forwarded verbatim as one `oid <hex>` argument per element. When `args.Size` is true the request also carries the `size` argument and each returned [ObjectInfo.Size] is populated with the server's reported size. When `args.Size` is false the Session sets every returned [ObjectInfo.Size] to `-1`, matching the "size not requested" sentinel documented on the ObjectInfo type itself. The library cannot distinguish a real zero-byte blob from a size the server elided on a Size-requested call, so a returned `Size == 0` is left as-is in the Size-requested branch.

`ObjectInfo` returns a slice, not an iterator, because the response is bounded by the caller-supplied `oids` count and can be safely materialised in memory. `Session.Refs`, by contrast, returns an iterator: an `ls-refs` response can be arbitrarily large and should be streamed rather than buffered.

Example

ExampleSession_ObjectInfo asks the server for the payload size of a handful of objects without fetching them — useful for supply-chain sizing, mirror estimates, or release-asset audits. `object-info` is a v2-only command; sessions that negotiated v0 or v1, or v2 servers that did not advertise the command, surface lsremote.ErrUnsupportedProtocol.

lsremote.ObjectInfo.Size is `-1` when the size was not requested or the server elided it; a real on-disk object can legitimately have a size of zero (an empty blob), so the negative sentinel is the only unambiguous "absent" marker.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	ctx := context.Background()
	session, err := lsremote.Dial(ctx,
		"https://github.com/octocat/Hello-World.git")
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	infos, err := session.ObjectInfo(ctx, []string{
		"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
		"553c2077f0edc3d5dc5d17262f6aa498e69d6f8e",
	}, lsremote.ObjectInfoRequest{Size: true})
	if err != nil {
		// A v2 server without object-info advertised is a common case
		// on hosts that only ship ls-refs and fetch; degrade gracefully.
		if errors.Is(err, lsremote.ErrUnsupportedProtocol) {
			fmt.Println("server does not support object-info")
			return
		}
		log.Fatal(err) //nolint:gocritic // example doc pattern: log.Fatal mirrors typical caller code
	}
	for _, info := range infos {
		fmt.Printf("%s %d bytes\n", info.Hash, info.Size)
	}
}

func (*Session) Refs

func (s *Session) Refs(ctx context.Context, args RefsRequest) (iter.Seq2[Ref, error], error)

Refs returns an iterator over the server's references.

On v2 (ProtocolV2) Refs issues an `ls-refs` command, forwarding [RefsRequest.Prefixes] verbatim as `ref-prefix` arguments and toggling `peel`, `symrefs`, and `unborn` per the corresponding RefsRequest flags. The `unborn` argument is dropped silently when the server did not advertise `ls-refs=unborn` in its capability list (connect.c::get_remote_refs lines 564-597). The iterator streams the response and yields one (Ref, error) pair per emission.

On v0/v1 the wire has no `ls-refs` equivalent: the full ref advertisement has already been consumed by Dial and cached on the Session. Refs filters that cached slice by [RefsRequest.Prefixes] client-side and yields the survivors. [RefsRequest.Peel] and [RefsRequest.Unborn] are still no-ops on v0/v1: peeled-tag information rides inline on the v0/v1 advertisement regardless, and v0/v1 has no unborn-`HEAD` wire representation.

[RefsRequest.Symrefs] is honoured client-side on v0/v1 even though it has no wire effect: when the flag is true the library post-fills [Ref.Symref] on each yielded ref from [Capabilities.Symrefs], unifying the call-site experience with v2. When the flag is false, [Ref.Symref] is always empty on yielded refs, regardless of what the advertisement carried. [Capabilities.Symrefs] remains populated in both cases for callers who prefer the capability-level view.

The returned error is non-nil only when the v2 command request itself fails before any response bytes are consumed (for example a transport-level POST failure). A wire-level decode error mid-stream surfaces through the iterator: it yields a single (zero Ref, err) pair wrapping the cause in a `*ProtocolError` with `Op == "ls-refs"` and stops. Iteration over a successful v0/v1 path never yields an error.

Example

ExampleSession_Refs walks the iterator returned by lsremote.Session.Refs and breaks out early. The library wraps the iterator so the connection state stays consistent on early `break`: the response is drained behind the scenes and the session remains usable for subsequent commands — load-bearing on the SSH, `git://`, and `file://` transports where one byte stream carries every command.

package main

import (
	"context"
	"fmt"
	"log"

	lsremote "github.com/hiddeco/go-ls-remote"
)

func main() {
	ctx := context.Background()
	session, err := lsremote.Dial(ctx,
		"https://github.com/octocat/Hello-World.git")
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	seq, err := session.Refs(ctx, lsremote.RefsRequest{
		Prefixes: []string{"refs/tags/"},
		Peel:     true,
	})
	if err != nil {
		log.Fatal(err) //nolint:gocritic // example doc pattern: log.Fatal mirrors typical caller code
	}

	// Take the first ten tags only.
	n := 0
	for ref, err := range seq {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(ref.Name)
		n++
		if n == 10 {
			break
		}
	}
}

type Symref

type Symref struct {
	// Name is the symbolic ref's own name, e.g. `HEAD`.
	Name string

	// Target is the ref name the symref points at, e.g.
	// `refs/heads/main`.
	Target string
}

Symref names a single symbolic-reference mapping that a v0/v1 server advertised in its capability list — for example `HEAD → refs/heads/main`.

v2 servers do not advertise symrefs at the capability level; they expose the same information inline on each Ref via [Ref.Symref] when the caller passes the `symrefs` argument to `ls-refs`. As a result [Capabilities.Symrefs] is populated only for v0/v1 handshakes and is left empty for v2.

Symref has no methods; callers read the exported fields directly.

Directories

Path Synopsis
internal
dumbhttp
Package dumbhttp synthesises a v0-shaped pkt-line stream from the `info/refs` body of a Git "dumb" HTTP server.
Package dumbhttp synthesises a v0-shaped pkt-line stream from the `info/refs` body of a Git "dumb" HTTP server.
inttest
Package inttest is the integration-test support layer that wires fixture repositories to in-process transports.
Package inttest is the integration-test support layer that wires fixture repositories to in-process transports.
livetest
Package livetest exercises the library end-to-end against real Git hosting providers.
Package livetest exercises the library end-to-end against real Git hosting providers.
objfmt
Package objfmt models Git's on-disk object formats: the hash algorithms in use, the typed object id values for each, and the pack object type enum.
Package objfmt models Git's on-disk object formats: the hash algorithms in use, the typed object id values for each, and the pack object type enum.
objstore
Package objstore is the read-only on-disk Git object store.
Package objstore is the read-only on-disk Git object store.
reftable
Package reftable reads Git reftable files: a stack-of-tables ref storage format with binary search inside each table and a merged view across the stack.
Package reftable reads Git reftable files: a stack-of-tables ref storage format with binary search inside each table and a merged view across the stack.
server
Package server implements an in-process emulator of canonical Git's `upload-pack` discovery-time wire protocols.
Package server implements an in-process emulator of canonical Git's `upload-pack` discovery-time wire protocols.
testfixture
Package testfixture materialises on-disk repository fixtures from `testdata/repos/<name>/` into a fresh `t.TempDir()`.
Package testfixture materialises on-disk repository fixtures from `testdata/repos/<name>/` into a fresh `t.TempDir()`.
wire
Package wire encodes and decodes the Git smart-protocol wire formats consumed by the root `lsremote` package.
Package wire encodes and decodes the Git smart-protocol wire formats consumed by the root `lsremote` package.
Package pktline implements the pkt-line wire-format codec used by the Git smart protocol in versions 0, 1, and 2.
Package pktline implements the pkt-line wire-format codec used by the Git smart protocol in versions 0, 1, and 2.
Package trace defines the Tracer interface and event types emitted by the go-ls-remote library at significant points in the wire-protocol and HTTP lifecycle.
Package trace defines the Tracer interface and event types emitted by the go-ls-remote library at significant points in the wire-protocol and HTTP lifecycle.
Package transport defines the abstraction every Git wire-protocol transport (HTTP, SSH, git daemon, file) implements.
Package transport defines the abstraction every Git wire-protocol transport (HTTP, SSH, git daemon, file) implements.
file
Package filet implements the local-filesystem Git transport.Transport.
Package filet implements the local-filesystem Git transport.Transport.
git
Package gitt implements the git-daemon transport for the `git://` URL scheme.
Package gitt implements the git-daemon transport for the `git://` URL scheme.
http
Package httpt is the HTTP/HTTPS Git transport.
Package httpt is the HTTP/HTTPS Git transport.
ssh
Package ssht is the SSH Git transport.
Package ssht is the SSH Git transport.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL