gitref

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package gitref carries a ticket on a git ref of its own, outside refs/heads.

The problem it solves: a ticket file lives in the branch of whoever wrote it. If that branch is unpushed, stale or force-pushed, the person the ticket was assigned to sees nothing — not the title, not even the id. Scanning every branch for ticket files is expensive and still wrong.

A ref under refs/jaira/tickets/<id> belongs to no branch. The default refspec does not fetch it, so a clone by someone who does not use jaira is unaffected, and the push onto it is a compare-and-swap that uses the ref lock the hosting provider already implements. No server, no daemon: the remote that is already there is the channel.

The ref's tree carries the ticket file itself rather than only its id, so a colleague reads the whole ticket without a branch and without a checkout:

git show refs/jaira/tickets/<id>:<id>.md

The ticket file also stays in the code commit, and the two answer different questions: the file in the commit says what this change was and why, the ref says where the ticket stands right now and with whom. Same bytes, one of them on a channel that is visible without the branch.

Index

Constants

View Source
const DefaultRemote = "origin"

DefaultRemote is where refs go when nothing is configured.

View Source
const Prefix = "refs/jaira/tickets/"

Prefix is the ref namespace. It is outside refs/heads deliberately: the default refspec does not fetch it, so these refs are invisible to anyone who does not ask for them, and they never show up as branches or in a hosting provider's web UI.

Variables

View Source
var (
	// ErrRaceLost means someone else wrote the ref after we read it. The
	// caller has stale bytes and must re-read; it is not a transport failure
	// and retrying the same push would only lose again.
	ErrRaceLost = errors.New("gitref: the ticket moved since it was read")

	// ErrOffline means the remote could not be reached at all. The write is
	// not lost and not refused — it is unsent, which is what the outbox is
	// for.
	ErrOffline = errors.New("gitref: the remote is unreachable")

	// ErrNoRef means the ticket has no ref yet.
	ErrNoRef = errors.New("gitref: no ref for this ticket")

	// ErrNoRepo means this board is not in a git repository, or the remote it
	// was told to use does not exist. It is not a failure to report at every
	// write: a board without a repository is a supported way to use jaira, it
	// simply does not carry tickets on refs.
	ErrNoRepo = errors.New("gitref: no repository or no such remote")

	// ErrNoGit means the git binary is unavailable.
	ErrNoGit = errors.New("gitref: git is not available on PATH")
)

Functions

func RefName

func RefName(id string) string

RefName is the ref a ticket travels on.

Types

type Repo

type Repo struct {
	Dir    string
	Remote string

	// Author names the identity written onto the ref commit. It is separate
	// from git config on purpose: these commits are made in the background of
	// somebody else's command, and a machine with no user.name configured (CI,
	// a fresh container) must still be able to write a ticket rather than fail
	// with git's "please tell me who you are".
	AuthorName  string
	AuthorEmail string
}

Repo is a working tree together with the remote its ticket refs live on.

func (*Repo) BlobsOf

func (r *Repo) BlobsOf(ids []string) (map[string]string, error)

BlobsOf resolves the blob each ticket ref carries, in one git invocation.

A snapshot reuses these blobs rather than hashing the content again: the objects are already in the repository, put there by the ref writes, so a snapshot adds a tree and a commit and nothing else.

func (*Repo) Commit

func (r *Repo) Commit(tree, parent, message string) (string, error)

Commit writes a commit object for a tree, optionally with a parent.

func (*Repo) Delete

func (r *Repo) Delete(id, lease string) error

Delete removes the ticket's ref, locally and on the remote. A logged or archived ticket is off the board, and leaving its ref behind would keep it on everybody else's.

func (*Repo) Fetch

func (r *Repo) Fetch() error

Fetch brings every ticket ref up to date in one roundtrip. The refspec is explicit because the default one does not carry these refs — which is the property that keeps them invisible to people who do not use jaira.

--prune is part of the contract, not a tidy-up: a ticket that was logged or archived deletes its ref, and without pruning that ticket would sit on everyone else's board forever, deleted for its owner and immortal for everyone else.

func (*Repo) FetchBranch

func (r *Repo) FetchBranch(branch string) error

FetchBranch brings one branch from the remote into a local ref of the same name, so a snapshot can be appended to what the remote already has.

func (*Repo) Landed

func (r *Repo) Landed(id string, branches []string) string

Landed reports the commit in which a ticket was filed away on one of the given branches, or "" if it has not arrived there.

Only the filed-away paths count. A ticket still sitting under tickets/ on that branch has not landed in this sense: it is on the board there, which is the opposite of finished.

func (*Repo) List

func (r *Repo) List() ([]string, error)

List returns the ticket ids that have a local ref.

func (*Repo) ListRemote

func (r *Repo) ListRemote() ([]string, error)

ListRemote asks the remote which tickets exist, without fetching them. One roundtrip is cheap enough to run before a command needs the contents.

func (*Repo) LsTree

func (r *Repo) LsTree(rev, dir string) ([]string, error)

LsTree lists the file names directly under dir in a revision.

func (*Repo) Nest

func (r *Repo) Nest(dir, tree string) (string, error)

Nest wraps a tree under a single directory name and returns the outer tree.

func (*Repo) PushBranch

func (r *Repo) PushBranch(branch, commit, lease string) error

PushBranch moves a branch on the remote to commit, refusing if the remote moved since lease was read. Used for the snapshot branch, where two people snapshotting at once must not overwrite one another.

func (*Repo) Read

func (r *Repo) Read(id string) ([]byte, string, error)

Read returns the ticket file carried by the local ref.

func (*Repo) ReadMany

func (r *Repo) ReadMany(ids []string) (map[string][]byte, error)

ReadMany returns the ticket files carried by many refs, in two git invocations rather than two per ticket.

Measured on this project: reading a hundred tickets one at a time costs about a quarter of a second, almost all of it process startup, while one cat-file --batch pass is not measurable. The board reads every ticket on every refresh, so the per-ticket shape would grow into the one thing this tool must never be — slow to open.

func (*Repo) ReadParent

func (r *Repo) ReadParent(id string) ([]byte, error)

ReadParent returns the ticket file as it was one commit earlier on the ref — the state this clone had already seen when it last wrote or fetched.

This is what makes reconciling the local file with the ref a real three-way merge rather than a guess about which side is newer: because every ref commit takes the leased SHA as its parent, the parent's blob is exactly "the state both sides started from". A ref with no parent (the ticket's first write) has no such state, and reports ErrNoRef so the caller can fall back to two-way.

func (*Repo) RemoteHead

func (r *Repo) RemoteHead() string

RemoteHead returns the remote's default branch as a revision (origin/master and the like), or "" if the clone does not know it.

A clone of a repository that has commits knows this without asking anybody; a clone of an empty one does not, and cannot be told until the remote has a branch at all. So this answers from what is already local, and SetRemoteHead is the separate, network-touching way to fill it in later.

func (*Repo) Rev

func (r *Repo) Rev(rev string) string

Rev resolves a revision to a SHA, or "" when it does not exist.

func (*Repo) SHA

func (r *Repo) SHA(id string) (string, error)

SHA returns the commit the ticket's local ref points at, or ErrNoRef. This SHA is the lease: every write is a compare-and-swap against the value the writer last read.

func (*Repo) SetRemoteHead

func (r *Repo) SetRemoteHead() error

SetRemoteHead asks the remote which branch it points HEAD at and records it, so RemoteHead can answer locally from then on. One round trip, ever.

func (*Repo) Tree

func (r *Repo) Tree(entries map[string]string) (string, error)

Tree writes a tree object holding the given entries, and returns its hash. The entries are name to blob, all regular files.

func (*Repo) TreeOf

func (r *Repo) TreeOf(rev string) string

TreeOf returns the tree a commit points at, or "" when the commit is unknown.

func (*Repo) Usable

func (r *Repo) Usable() error

Usable reports whether this working tree can carry tickets on refs at all: git present, inside a repository, and the remote actually configured.

Asked once by the caller rather than inferred from a failed write, because the two are different answers to different questions — "this board does not use refs" is a permanent property of the checkout, while a refused push is about one ticket at one moment.

func (*Repo) Write

func (r *Repo) Write(id string, content []byte, lease string) (string, error)

Write puts the ticket file on the ref and pushes it, refusing if the remote moved since lease was read.

The commit takes the leased SHA as its parent when there is one, so the ref keeps the ticket's history (git log on the ref is the ticket's movement) and a stale write is a non-fast-forward that git rejects on its own. --force-with-lease is still passed: it is the check that holds when a ticket is written for the first time by two people at once, where neither commit has a parent to compare.

An empty lease means "no ref yet" and requires the remote to have none either.

Jump to

Keyboard shortcuts

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