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 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) Fetch() error
- func (r *Repo) FetchBranch(branch string) error
- func (r *Repo) Landed(id string, branches []string) string
- func (r *Repo) List() ([]string, error)
- func (r *Repo) ListRemote() ([]string, error)
- func (r *Repo) LsTree(rev, dir 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) ReadMany(ids []string) (map[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)
Constants ¶
const DefaultRemote = "origin"
DefaultRemote is where refs go when nothing is configured.
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 ¶
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 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) 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) 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) 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) 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) 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.