epic

package
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 10 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.

Index

Constants

This section is empty.

Variables

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 ErrNotFound = errors.New("no such epic")

ErrNotFound means no epic of that name exists.

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 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 and description 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.

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
	// 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 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.

func Create

func Create(s *ticket.Store, r Refs, name, title, description string, ids []string) (*Epic, error)

Create writes a new epic with its first members. 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 (*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) Bytes

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

Bytes renders the epic as the file its ref carries.

func (*Epic) Has

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

Has reports whether a ticket id is a member.

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.

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"`
}

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