gitref

package
v0.5.8 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 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 EpicPrefix = Root + "epics/"

EpicPrefix is where an epic travels — and, unlike a milestone, the only place it lives: an epic has no working file on disk. The member list is written by whoever sorts a ticket into it, often while an agent works that ticket, so it must never share a file with anything a branch carries.

View Source
const MigratedFromTrailer = "Jaira-Migrated-From"

MigratedFromTrailer is the message trailer of the commit that moved a milestone to its ID ref: the tip of the old name ref it came from, or "none".

View Source
const MilestonePrefix = Root + "milestones/"

MilestonePrefix is where a milestone file travels. Same reasoning as a ticket's ref, for the same reason: the group a round of work is planned in has to reach everybody without waiting for a branch to be merged, or the one file everybody is supposed to read is the one file nobody has.

View Source
const Prefix = Root + "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.

View Source
const Root = "refs/jaira/"

Root is the namespace every kind of ref lives under. There are several kinds because different things travel this way: a ticket, keyed by its id, and a milestone or an epic, keyed by its name. They are kept apart by a path segment rather than sharing one flat space — a milestone called "0YGWXQ" and a ticket whose handle is 0YGWXQ would otherwise be one ref, and whichever was written last would silently be the other one's content.

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")

	// ErrNoGitRepo is the first of those two cases on its own: there is no git
	// repository here at all, so no remote is missing and none can be
	// configured.
	//
	// It wraps ErrNoRepo rather than replacing it so that every existing
	// errors.Is(err, ErrNoRepo) keeps answering yes. The distinction exists for
	// the one caller that has to say something: advice about a missing remote
	// and about 'git config jaira.remote' is wrong where there is no
	// repository, and pointing at a command that pushes a ref is worse than
	// saying nothing.
	ErrNoGitRepo = fmt.Errorf("%w: this board is not in a git repository", ErrNoRepo)

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

Functions

func BoardRemote added in v0.2.1

func BoardRemote(dir string) string

BoardRemote returns the remote this one clone was told to use for its ticket refs, from git config jaira.remote, or "" when nothing was set.

--local on purpose: the value has to belong to this clone and no other. A global git config would be the same mistake as the machine-wide setting it exists to correct, and worktrees share the clone's config, so every worktree of a checkout answers the same without anybody repeating themselves.

func EpicRefName added in v0.5.1

func EpicRefName(name string) string

EpicRefName is the ref an epic travels on.

func MilestoneRefName added in v0.3.0

func MilestoneRefName(name string) string

MilestoneRefName is the ref a milestone travels on.

func RefName

func RefName(id string) string

RefName is the ref a ticket travels on.

func Remotes added in v0.2.1

func Remotes(dir string) []string

Remotes lists the remote names this repository actually has, in git's own order. A directory that is not a repository, or one with no remotes at all, answers with nothing — the question "which remotes are there" has an empty answer in both cases, and the caller that needs to tell them apart asks Usable.

It is a function on a directory rather than a method on Repo because the answer is what decides which remote a Repo should be built with in the first place.

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) EpicSHA added in v0.5.1

func (r *Repo) EpicSHA(name string) (string, error)

EpicSHA is the lease for an epic's ref.

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) Had added in v0.5.7

func (r *Repo) Had(id string, content []byte) bool

Had reports whether content is a state the ref of this ticket has carried: its blob is reachable from the ref. False when there is no ref, and on any error.

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) ListEpics added in v0.5.1

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

ListEpics returns the epic names that have a local ref.

func (*Repo) ListHashes added in v0.5.0

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

ListHashes is List with the commit each ref points at, in one git invocation. A ref's commit hash is its content's address, so a caller that remembers the hash it last decoded knows without reading anything whether the ticket changed.

func (*Repo) ListMilestones added in v0.3.0

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

ListMilestones returns the milestone names 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) MilestoneHad added in v0.5.0

func (r *Repo) MilestoneHad(key string, content []byte) bool

MilestoneHad reports whether content is a state the ref of this milestone key has carried: its blob is reachable from the ref. False when there is no ref, and on any error.

func (*Repo) MilestoneSHA added in v0.3.0

func (r *Repo) MilestoneSHA(name string) (string, error)

MilestoneSHA is the lease for a milestone's ref, exactly as SHA is for a ticket's.

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) NoRemoteHint added in v0.2.1

func (r *Repo) NoRemoteHint() string

NoRemoteHint explains a missing remote the way somebody can act on it.

It is exported for the same reason RemoteName is: a command that has to tell a person why this board carries no refs must be able to ask for the sentence instead of reconstructing it — or, worse, cutting it back out of the error text and so depending on how Usable happened to concatenate it.

"no remote \"upstream\"" was true and useless: it named the thing that is absent and nothing else, so the reader still had to find out what this checkout does have and where the name even came from — and the name usually came from a machine-wide setting made for a different repository. So the message carries all three: the name that was asked for, the remotes that are really here, and the one command that settles it for this clone.

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) ReadEpic added in v0.5.1

func (r *Repo) ReadEpic(name string) ([]byte, string, error)

ReadEpic returns the epic file carried by the local ref, with its lease.

func (*Repo) ReadEpicAt added in v0.5.1

func (r *Repo) ReadEpicAt(name, sha string) ([]byte, error)

ReadEpicAt returns the epic file as one commit of its ref carried it. A lost race is rebased from here: the commit the losing write was leased against is the state both sides started from.

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) ReadMigration added in v0.5.0

func (r *Repo) ReadMigration(key string) (content []byte, from string, err error)

ReadMigration is the migration commit on the ID ref: the first-parent commit that carries the trailer. It returns the content that commit holds and the old name ref's tip it came from ("" when there was none), and ErrNoRef when the ref has no such commit, which is a milestone created with its ID.

func (*Repo) ReadMilestone added in v0.3.0

func (r *Repo) ReadMilestone(key string) (file string, content []byte, sha string, err error)

ReadMilestone returns the file name the local ref carries its milestone under, the file, and the SHA that is the lease for writing it back. The name is read from the tree rather than derived from the key, because a milestone's ref is keyed by something that is not its file name.

func (*Repo) ReadMilestoneFileAt added in v0.5.0

func (r *Repo) ReadMilestoneFileAt(sha string) (string, []byte, error)

ReadMilestoneFileAt returns the milestone file the commit sha carries, and the name it carries it under: the state a write was leased on, which is what a refused write is merged from.

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) RemoteName added in v0.2.1

func (r *Repo) RemoteName() string

RemoteName is the remote this repo actually talks to, with the default filled in. Exported because a command that has to explain which remote was looked for must not have to re-derive it and risk naming a different one than the code that failed.

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.

func (*Repo) WriteEpic added in v0.5.1

func (r *Repo) WriteEpic(name string, content []byte, lease string) (string, error)

WriteEpic puts an epic file on its ref and pushes it, under the same compare-and-swap as a milestone.

func (*Repo) WriteMigration added in v0.5.0

func (r *Repo) WriteMigration(key, file string, migrated []byte, from, editFile string, edit []byte) (string, error)

WriteMigration is the first write of a milestone's ID ref when an older milestone file moved to it: a commit of its own holding only the migrated content, and in its message the old name ref it came from (from, a SHA or "none"). An edit made since goes on top of it as a second commit in the same push, so the migration stays readable from the ref (ReadMigration).

func (*Repo) WriteMilestone added in v0.3.0

func (r *Repo) WriteMilestone(key, file string, content []byte, lease string) (string, error)

WriteMilestone puts a milestone file on its ref and pushes it, under the same compare-and-swap as a ticket. A milestone is edited by hand as often as by jaira, so losing a race here means the same thing it means for a ticket: the caller's bytes are stale and must be re-read, never force-pushed over.

Jump to

Keyboard shortcuts

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