Documentation
¶
Overview ¶
Package epic holds the features a board is built out of: a description for a person and the tickets that make it up, kept on a ref of its own, refs/jaira/epics/<name>, and nowhere else.
An epic is not a ticket. It has no definition of done, runs through no lane and is worked by no agent — it is a description plus a list. Nor is it a milestone: a milestone is a round of work, and what is unfinished moves on to the next one; an epic is a feature, and what is unfinished stays in it.
The member list is in the epic, never on the tickets. Sorting a ticket into an epic therefore writes the epic's ref alone, and never the ref of a ticket an agent may be working at that very moment — so the two cannot conflict.
There is no working file. The ref is read directly, and an edit that has not reached the remote yet is read back out of the outbox, which is the only copy of it. Both are behind the Refs interface, so nothing in here talks to git.
The file the ref carries is read and written the way a milestone file is: frontmatter for the facts, one "- <ticket id>" line per member below it, and every other line kept verbatim across a write, so a person may edit it by hand and read it in a diff.
A finished epic is closed, never deleted: "status: closed" in its frontmatter, the way a filed milestone says "status: filed". The ref stays, so every clone reads the closing on its next fetch and 'jaira epic reopen' has something to take back. A closed epic takes no new tickets unless asked with force, and drops out of the default listings.
Index ¶
- Variables
- func Covered(members []string, ix *link.Index) []string
- func LabelOf(r Refs, name string) string
- func LinkableURL(raw string) bool
- func Merge(name string, base, ours, theirs []byte) []byte
- func NormalizeName(raw string) (string, bool, error)
- func Unassigned(epics []*Epic, ix *link.Index, board []*ticket.Ticket) []*ticket.Ticket
- type Epic
- func Assign(s *ticket.Store, r Refs, name string, ids []string, add, force bool) (*Epic, []string, error)
- func Close(s *ticket.Store, r Refs, name string, ix *link.Index, force bool) (*Epic, bool, error)
- func Create(s *ticket.Store, r Refs, name, title, description string, ids []string, ...) (*Epic, error)
- func FromBytes(name string, content []byte) *Epic
- func Load(r Refs, name string) (*Epic, error)
- func LoadAll(r Refs) ([]*Epic, error)
- func New(name, title, description string, now time.Time) *Epic
- func Reopen(s *ticket.Store, r Refs, name string) (*Epic, bool, error)
- func Set(s *ticket.Store, r Refs, name string, p Patch) (*Epic, bool, error)
- func (e *Epic) Add(id string) bool
- func (e *Epic) Body() string
- func (e *Epic) Bytes() []byte
- func (e *Epic) Externals() []External
- func (e *Epic) Fields() Fields
- func (e *Epic) Has(id string) bool
- func (e *Epic) HasExternal(key string) bool
- func (e *Epic) Label() string
- func (e *Epic) Members() []string
- func (e *Epic) Remove(id string) bool
- func (e *Epic) SetBody(text string)
- func (e *Epic) Text() string
- type External
- type Fields
- type Patch
- type Progress
- type Refs
- type Ticket
Constants ¶
This section is empty.
Variables ¶
var ErrBadExternal = errors.New("an external key is '<KEY> [<URL>]', the URL http or https")
ErrBadExternal means a key given on the command line or from the page is not "<KEY> [<URL>]" with an http or https URL. Nothing was written.
var ErrChanged = errors.New("epic changed since it was read")
ErrChanged means the epic was changed since the editor read it, in a field the editor meant to change. Nothing was written.
var ErrClosed = errors.New("epic is closed")
ErrClosed means a ticket was to be put into a closed epic without force. Nothing was written.
var ErrExists = errors.New("epic exists already")
ErrExists means an epic of that name exists already.
var ErrNoRefs = errors.New("epics live on git refs, and this board has no repository with a remote")
ErrNoRefs means the board has nowhere to keep an epic: no repository or no remote. Epics live on refs only, so there is no local fallback to offer.
var ErrNoTitle = errors.New("an epic's title cannot be empty")
ErrNoTitle means an edit would leave the epic without a title; the name is not one, it is where the epic is kept.
var ErrNotFound = errors.New("no such epic")
ErrNotFound means no epic of that name exists.
var ErrUnfinished = errors.New("epic has unfinished tickets")
ErrUnfinished means an epic was to be closed while tickets counted towards it are not finished, without force. Nothing was written.
Functions ¶
func Covered ¶
Covered expands a set of member ids by every ticket that follows on from one of them, transitively, members first in their order, follow-ups after in id order.
func LabelOf ¶ added in v0.5.10
LabelOf is Label for an epic known by name only, as an error path has it. An epic that cannot be read is named by its name.
func LinkableURL ¶ added in v0.5.10
LinkableURL reports whether a URL is one a page may make a link of: http or https with a host. A hand-edited file may carry anything; this is what decides whether it is shown as a link or as text.
func Merge ¶
Merge rebases an edit that lost the race for the ref. base is what the edit started from, ours the edit, theirs what the remote holds now. The result is theirs with our member additions and removals applied, and our title, description and text where we changed them — so two people sorting tickets into one epic at once both get their tickets in, and nobody's hand edit is lost.
Where both sides changed the same field to different values, ours is kept and theirs goes below the text in a marked block, for a person to merge by hand: a person's text has no right answer a machine can pick, and dropping either side would lose what somebody wrote without anybody knowing.
func NormalizeName ¶
NormalizeName turns a written epic name into the form it is stored under: lowercase kebab, the tag rules. The name is a ref path segment and the name of the file inside the ref, so it has to be safe in both.
func Unassigned ¶
Unassigned returns the open tickets of a board that belong to no epic, neither by a member line nor by following on from a member. board is the listing the caller already has; tickets in a terminal lane are left out, since finished work needs no home.
Types ¶
type Epic ¶
type Epic struct {
Name string
Title string
Description string
CreatedAt time.Time
// Closed is "status: closed" in the frontmatter.
Closed bool
// contains filtered or unexported fields
}
Epic is one epic as it was read: every line kept verbatim beside the facts parsed out of them.
func Assign ¶
func Assign(s *ticket.Store, r Refs, name string, ids []string, add, force bool) (*Epic, []string, error)
Assign puts tickets into an epic (add) or takes them out, in one write of the epic's ref and of nothing else. It returns the epic as written and the ids that actually changed; nothing is written when none did. ids must be full ticket ids.
A closed epic refuses a new member with ErrClosed unless force is set, and stays closed when it is: putting a straggler into a finished feature is a correction, not a reopening. Taking a ticket out is never refused.
func Close ¶ added in v0.5.9
Close marks an epic closed, in one write of its ref. An epic that still counts an unfinished ticket — a member or a follow-up, as Count says — is refused with ErrUnfinished unless force is set. It returns the epic and whether anything was written; closing a closed epic writes nothing.
func Create ¶
func Create(s *ticket.Store, r Refs, name, title, description string, ids []string, external ...External) (*Epic, error)
Create writes a new epic with its first members and external keys. The name must be normalized already, and ids must be full ticket ids.
func FromBytes ¶
FromBytes reads an epic out of what its ref carries. The name comes from the ref, never from the content: a name inside the file would be a second truth that an edit could make disagree with the ref it lies on.
func LoadAll ¶
LoadAll reads every epic, sorted by name. An epic whose ref cannot be read is skipped rather than failing the listing.
func Reopen ¶ added in v0.5.9
Reopen takes the closing back, in one write of its ref. It returns the epic and whether anything was written; an open epic writes nothing.
func Set ¶ added in v0.5.5
Set rewrites an epic's title, description and text, in one write of its ref and leaving its members as they are. It returns the epic as it stands afterwards and whether anything was written.
func (*Epic) Add ¶
Add appends one member line. A ticket already in the epic is left where it is: re-adding must not reorder a list somebody arranged by hand.
func (*Epic) Body ¶ added in v0.5.5
Body is the markdown below the frontmatter, every member line taken out and the blank lines around it trimmed: the text a person wrote about the feature. The member lines are the list, and the list is edited with Add and Remove, not as text.
func (*Epic) HasExternal ¶ added in v0.5.10
HasExternal reports whether the epic carries a key, in any case.
func (*Epic) Label ¶ added in v0.5.10
Label is how a message names the epic: its title, which a person reads, with the name beside it, which a person types into a command — "Web 0.2" (web-0-2). An epic whose title is its name is named once.
type External ¶ added in v0.5.10
External is the key of the task in another system that an epic stands for — JIRA-1234, a YouTrack issue — and where to open it. It sits on the epic and never on a ticket: the ticket file is committed with the code, the epic's ref is not, so a key kept for billing never ends up in a code commit.
The frontmatter holds them as a block list, one key per line:
external: - JIRA-1234 https://jira.example.com/browse/JIRA-1234 - YT-77
URL may be empty: a key without a link is still something to filter by.
func ParseExternal ¶ added in v0.5.10
ParseExternal reads "<KEY> [<URL>]" as a person typed it. It is strict where the file reader is lenient: what jaira writes itself is a key of one word and a link a page can open, never a javascript: or file: one.
type Fields ¶ added in v0.5.5
type Fields struct {
Title, Description, Body string
}
Fields are what a person writes about an epic: the title, the one-line description and the text below them.
type Patch ¶ added in v0.5.5
type Patch struct {
Title, Description, Body *string
// AddExternal puts keys on the epic, a key it carries already getting
// the new URL; DropExternal takes keys off, in any case. Both are
// changes rather than a new list, so two people adding a key each at
// once both get theirs, and neither needs Base.
AddExternal []External
DropExternal []string
// Base is what the editor read before it changed anything; nil writes
// onto whatever is there. With it, a field the patch changes that is no
// longer what Base says refuses the whole write with ErrChanged, so an
// editor that sat open while somebody else saved cannot overwrite them.
Base *Fields
}
Patch is an edit of an epic: a nil field is left as it is.
type Progress ¶
type Progress struct {
Done int `json:"done"`
Total int `json:"total"`
Tickets []Ticket `json:"tickets"`
}
Progress is how far an epic is.
func Count ¶
Count says how far an epic is: its members, plus every ticket that follows on from one of them through follows: or parent:, however many steps down, wherever it is filed. Done is the index's Satisfied — a terminal lane or the logbook — so the epic agrees with the gate about what finished means.
Follow-ups count without a line because they appear while the work goes on: closing one ticket often opens two, and a list that had to be kept up by hand would always be behind.
type Refs ¶
type Refs interface {
// EpicNames lists every epic, unsent ones included.
EpicNames() ([]string, error)
// EpicContent reads one epic: an unsent write if there is one, else the
// local ref. A missing epic wraps ErrNotFound.
EpicContent(name string) ([]byte, error)
// RecordEpic queues an epic's whole content for its ref.
RecordEpic(name string, content []byte) error
}
Refs is where epics are kept. *refsync.Syncer implements it; a test can hand in a map.
type Ticket ¶
type Ticket struct {
ID string `json:"id"`
Title string `json:"title"`
Lane string `json:"lane"`
Done bool `json:"done"`
// Direct is true for a member line, false for a ticket that counts
// because it follows on from one (follows: or parent:).
Direct bool `json:"direct"`
}
Ticket is one ticket counted towards an epic.