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
- Variables
- func BoardRemote(dir string) string
- func EpicRefName(name string) string
- func MilestoneRefName(name string) string
- func RefName(id string) string
- func Remotes(dir string) []string
- type Repo
- func (r *Repo) BlobsOf(ids []string) (map[string]string, error)
- func (r *Repo) Commit(tree, parent, message string) (string, error)
- func (r *Repo) Delete(id, lease string) error
- func (r *Repo) EpicSHA(name string) (string, error)
- func (r *Repo) Fetch() error
- func (r *Repo) FetchBranch(branch string) error
- func (r *Repo) Had(id string, content []byte) bool
- func (r *Repo) Landed(id string, branches []string) string
- func (r *Repo) List() ([]string, error)
- func (r *Repo) ListEpics() ([]string, error)
- func (r *Repo) ListHashes() (map[string]string, error)
- func (r *Repo) ListMilestones() ([]string, error)
- func (r *Repo) ListRemote() ([]string, error)
- func (r *Repo) LsTree(rev, dir string) ([]string, error)
- func (r *Repo) MilestoneHad(key string, content []byte) bool
- func (r *Repo) MilestoneSHA(name string) (string, error)
- func (r *Repo) Nest(dir, tree string) (string, error)
- func (r *Repo) NoRemoteHint() string
- func (r *Repo) PushBranch(branch, commit, lease string) error
- func (r *Repo) Read(id string) ([]byte, string, error)
- func (r *Repo) ReadEpic(name string) ([]byte, string, error)
- func (r *Repo) ReadEpicAt(name, sha string) ([]byte, error)
- func (r *Repo) ReadMany(ids []string) (map[string][]byte, error)
- func (r *Repo) ReadMigration(key string) (content []byte, from string, err error)
- func (r *Repo) ReadMilestone(key string) (file string, content []byte, sha string, err error)
- func (r *Repo) ReadMilestoneFileAt(sha string) (string, []byte, error)
- func (r *Repo) ReadParent(id string) ([]byte, error)
- func (r *Repo) RemoteHead() string
- func (r *Repo) RemoteName() string
- func (r *Repo) Rev(rev string) string
- func (r *Repo) SHA(id string) (string, error)
- func (r *Repo) SetRemoteHead() error
- func (r *Repo) Tree(entries map[string]string) (string, error)
- func (r *Repo) TreeOf(rev string) string
- func (r *Repo) Usable() error
- func (r *Repo) Write(id string, content []byte, lease string) (string, error)
- func (r *Repo) WriteEpic(name string, content []byte, lease string) (string, error)
- func (r *Repo) WriteMigration(key, file string, migrated []byte, from, editFile string, edit []byte) (string, error)
- func (r *Repo) WriteMilestone(key, file string, content []byte, lease string) (string, error)
Constants ¶
const DefaultRemote = "origin"
DefaultRemote is where refs go when nothing is configured.
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.
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".
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.
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.
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 ¶
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
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
EpicRefName is the ref an epic travels on.
func MilestoneRefName ¶ added in v0.3.0
MilestoneRefName is the ref a milestone travels on.
func Remotes ¶ added in v0.2.1
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 ¶
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) Delete ¶
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 ¶
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 ¶
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
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 ¶
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) ListHashes ¶ added in v0.5.0
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
ListMilestones returns the milestone names that have a local ref.
func (*Repo) ListRemote ¶
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) MilestoneHad ¶ added in v0.5.0
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
MilestoneSHA is the lease for a milestone's ref, exactly as SHA is for a ticket's.
func (*Repo) NoRemoteHint ¶ added in v0.2.1
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 ¶
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) ReadEpic ¶ added in v0.5.1
ReadEpic returns the epic file carried by the local ref, with its lease.
func (*Repo) ReadEpicAt ¶ added in v0.5.1
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 ¶
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
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
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
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 ¶
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 ¶
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
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) SHA ¶
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 ¶
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 ¶
Tree writes a tree object holding the given entries, and returns its hash. The entries are name to blob, all regular files.
func (*Repo) Usable ¶
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 ¶
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
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
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.