milestone

package
v0.4.3 Latest Latest
Warning

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

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

Documentation

Overview

Package milestone holds the groups a board plans in: which tickets belong together for this round of work, kept in one file per group under .jaira/milestones/.

The grouping lives in its own file rather than as a field on each ticket because of what happens at the end of a round. Work that did not finish has to appear in the next group, and a per-ticket marker means touching every ticket one at a time — and tickets travel on their own refs, so that is one pull, one edit and one release each. Twenty of those is the reason nobody does it. A file is copied instead: the lines that are not finished move into the next milestone's file in one edit.

A milestone that has been filed into the logbook carries `status: filed` in its frontmatter, and that line is what keeps it off the board. It is written onto the ref too, on purpose: a ref taken down would free the name again and tell no other clone anything, while a ref saying "filed" is read by every clone that fetches — it stops the file being written back to disk, and it holds the name until somebody restores it. No line means the milestone is on the board, which is the state every file written before this existed is in.

The file is read and written the way a ticket file is — frontmatter for the facts about the milestone, one line per member below it — and the lines are kept verbatim across a write. A comment, a blank line, a hand-chosen order and a line jaira cannot parse all survive Add and Remove, because those edit one line and leave the rest alone. The format is the API: it must stay hand-editable and readable in a diff, and one line per member is the smallest thing git can merge.

Index

Constants

View Source
const StatusFiled = "filed"

StatusFiled is the one status a milestone can carry. A milestone is either on the board or filed into the logbook, so the field is a flag with a name rather than a state machine — and a name, because a reader of the file has to be able to tell what it means without a table.

Subdir is where a board keeps its milestones, under .jaira. It is the name core/ticket files a milestone under inside a logbook folder too, and is taken from there so the two cannot drift apart.

Variables

View Source
var Palette = tag.Palette

Palette is the set of colours a new milestone can be given. It is the tag palette: the two marks sit on opposite edges of a card and are never confused for one another by position, so spending a second set of sixteen values would only push both closer to the colours the board already spends on status.

Functions

func AssignColour

func AssignColour(existing []*Milestone, name string) int

AssignColour picks a colour for a new milestone: a random one none of the existing milestones is using, so two groups on one board stay distinguishable. Random rather than next-in-sequence because a sequence gives every board the same first colours, and nobody should have to choose one. Once the palette is spent it repeats, derived from the name so the repeat is at least stable across machines.

func Dir

func Dir(root string) string

Dir is the milestone directory of a board rooted at root.

func NormalizeName

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

NormalizeName turns a written milestone name into the form it is stored and filed under: lowercase kebab, exactly the tag rules. A milestone name is a filename, so the charset has to be one that is safe in a path on every platform — and reusing the tag rules means one answer to "what may I call this" instead of two that differ in a corner.

func Path

func Path(root, name string) string

Path is the file one milestone lives in.

Types

type Index

type Index map[string][]*Milestone

Index answers, for one ticket id, which milestones hold it — the direction every reader needs and the file does not give. Multiple membership is allowed: a ticket carried over from one round into the next is in both files until somebody removes the old line.

func Build

func Build(all []*Milestone) Index

Build indexes milestones by ticket id, keeping the board's file order within each ticket so the marks on a card are painted the same way everywhere.

func (Index) For

func (i Index) For(id string) []*Milestone

For returns the milestones holding a ticket.

func (Index) Matches

func (i Index) Matches(id, want string) bool

Matches reports whether a ticket belongs to the named milestone. The comparison is equality on the normalized name, not a substring: a milestone is a name from a closed set, and a filter is a question whose answer to an impossible name is "no".

func (Index) Names

func (i Index) Names(id string) []string

Names returns the milestone names holding a ticket.

type Milestone

type Milestone struct {
	Name      string
	Colour    int
	CreatedAt time.Time
	Status    string
	// contains filtered or unexported fields
}

Milestone is one file as it was read: every line kept verbatim beside the facts parsed out of them.

func FromBytes

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

FromBytes reads a milestone out of bytes that are not on this disk — what a ref carries. The ref is the only place a milestone filed on another machine can be read from, and the caller has to be able to ask its status without first writing it to the board, which is the very thing a filed milestone must not do.

func Load

func Load(root, name string) (*Milestone, error)

Load reads one milestone by name. A missing file reports os.ErrNotExist, so a caller can tell "no such milestone" from "unreadable".

The file name is the milestone's name, always — the frontmatter does not carry one. The file is hand-editable by design, and a name written inside it would be a second truth: edit that line and the next Save files the milestone under a different name, leaving the old file and its ref behind.

func LoadAll

func LoadAll(root string) ([]*Milestone, error)

LoadAll reads every milestone that is ON the board, sorted by name. An absent directory is not an error: it means the board has planned no milestones yet, which is the state every board starts in.

A filed milestone is not on the board and is left out here, which is what makes `status: filed` the line that decides it rather than where the file happens to lie. Both states put a file under .jaira/milestones/: filing it moves it into the logbook, but a fetch writes the marked file back the way it writes every other one, and a filing that got as far as marking the file and no further has left it lying here too. One read of the line answers all three. Load reads a filed milestone by name, which is how a restore gets at it.

A file that does not parse is skipped rather than failing the read: the board is glanced at constantly, and one malformed file must not be able to stop it from opening.

func New

func New(name string, colour int, now time.Time) *Milestone

New builds a milestone that has never been written, with the colour the caller picked. It is not on disk until Save.

func (*Milestone) Add

func (m *Milestone) Add(id string) bool

Add records a ticket as a member, appending one line. A ticket already in the file is left where it is: re-adding must not reorder a list somebody arranged by hand.

func (*Milestone) Bytes

func (m *Milestone) Bytes() []byte

Bytes renders the milestone as the file it is: every line as it was read or edited, newline-terminated. It is what Save writes, and it is what the ref carries — the caller that queues the ref already holds the milestone, so it spells the content from here rather than reading back the file Save just wrote.

func (*Milestone) Filed

func (m *Milestone) Filed() bool

Filed reports whether this milestone has been put into the logbook. A filed milestone is off the board: 'jaira milestone ls' does not name it, no card carries its colour, and a fetch that finds its ref leaves the ref alone rather than writing the file back to disk.

func (*Milestone) Has

func (m *Milestone) Has(id string) bool

Has reports whether a ticket id is a member.

func (*Milestone) HasColour

func (m *Milestone) HasColour() bool

HasColour reports whether this milestone paints a cell on a card. Zero is "no colour" rather than ANSI black: the board reserves the right-hand column whether a ticket is in a milestone or not, so something has to mean "leave the cell blank", and 0 is that value everywhere it is asked.

func (*Milestone) Members

func (m *Milestone) Members() []string

Members lists the ticket ids this milestone holds, in the order the file gives them. File order is the order the card's marks are painted in, so it is a thing a person controls by editing.

func (*Milestone) Remove

func (m *Milestone) Remove(id string) bool

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

func (*Milestone) Save

func (m *Milestone) Save(root string) error

Save writes the milestone back through ticket.WriteAtomic, so a reader never sees a half-written file and a crash leaves the previous one intact. Atomicity is not exclusion: two writers still have to be serialised by the caller.

func (*Milestone) SetStatus

func (m *Milestone) SetStatus(status string)

SetStatus writes, replaces or removes the status line in the frontmatter and touches nothing else — the same rule Add and Remove follow. Rebuilding the frontmatter from the parsed facts would be shorter and would throw away the comments and the order somebody chose, which is exactly what the format promises to keep.

The empty status removes the line, which is how a restore puts a milestone back on the board. A file with no frontmatter at all is given one, because there is nowhere else for the status to go and a hand-written file is allowed to have started without it.

Jump to

Keyboard shortcuts

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