epic

package
v0.5.12 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 13 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
var ErrClosed = errors.New("epic is closed")

ErrClosed means a ticket was to be put into a closed epic without force. Nothing was written.

View Source
var ErrExists = errors.New("epic exists already")

ErrExists means an epic of that name exists already.

View Source
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.

View Source
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.

View Source
var ErrNotFound = errors.New("no such epic")

ErrNotFound means no epic of that name exists.

View Source
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

func Covered(members []string, ix *link.Index) []string

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

func LabelOf(r Refs, name string) string

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

func LinkableURL(raw string) bool

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

func Merge(name string, base, ours, theirs []byte) []byte

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

func NormalizeName(raw string) (string, bool, error)

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

func Unassigned(epics []*Epic, ix *link.Index, board []*ticket.Ticket) []*ticket.Ticket

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

func Close(s *ticket.Store, r Refs, name string, ix *link.Index, force bool) (*Epic, bool, error)

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

func FromBytes(name string, content []byte) *Epic

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 Load

func Load(r Refs, name string) (*Epic, error)

Load reads one epic.

func LoadAll

func LoadAll(r Refs) ([]*Epic, error)

LoadAll reads every epic, sorted by name. An epic whose ref cannot be read is skipped rather than failing the listing.

func New

func New(name, title, description string, now time.Time) *Epic

New builds an epic that has never been written.

func Reopen added in v0.5.9

func Reopen(s *ticket.Store, r Refs, name string) (*Epic, bool, error)

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

func Set(s *ticket.Store, r Refs, name string, p Patch) (*Epic, bool, error)

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

func (e *Epic) Add(id string) bool

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

func (e *Epic) Body() string

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

func (e *Epic) Bytes() []byte

Bytes renders the epic as the file its ref carries.

func (*Epic) Externals added in v0.5.10

func (e *Epic) Externals() []External

Externals lists the epic's keys in file order.

func (*Epic) Fields added in v0.5.5

func (e *Epic) Fields() Fields

Fields reads the three back.

func (*Epic) Has

func (e *Epic) Has(id string) bool

Has reports whether a ticket id is a member.

func (*Epic) HasExternal added in v0.5.10

func (e *Epic) HasExternal(key string) bool

HasExternal reports whether the epic carries a key, in any case.

func (*Epic) Label added in v0.5.10

func (e *Epic) Label() string

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.

func (*Epic) Members

func (e *Epic) Members() []string

Members lists the ticket ids this epic holds, in file order.

func (*Epic) Remove

func (e *Epic) Remove(id string) bool

Remove drops a member's line, keeping everything around it.

func (*Epic) SetBody added in v0.5.5

func (e *Epic) SetBody(text string)

SetBody replaces the text below the frontmatter. The member lines are kept verbatim and in their order, after the new text: whatever the text says, the list is what it was.

func (*Epic) Text added in v0.5.5

func (e *Epic) Text() string

Text is the body as a person reads it beside the title: the heading that only repeats the title and the comments meant for whoever edits the file left out. Body is what an editor gets, so that a round trip keeps them.

type External added in v0.5.10

type External struct {
	Key string `json:"key"`
	URL string `json:"url"`
}

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

func ParseExternal(raw string) (External, error)

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.

func (External) String added in v0.5.10

func (x External) String() string

String is the key as its line in the file has it.

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

func Count(e *Epic, ix *link.Index) Progress

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"`
	// Via is, for a follow-up, the member it is counted through: the line
	// that would have to go to take it out of the epic. Empty for a member.
	Via string `json:"via,omitempty"`
}

Ticket is one ticket counted towards an epic.

Jump to

Keyboard shortcuts

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