milestone

package
v0.5.7 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 18 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 tell no other clone anything, while a ref saying "filed" is read by every clone that fetches — it is not written back to a clone that never had it. No line means the milestone is on the board, which is the state every file written before this existed is in. A filed milestone does not hold its name: the ID is what tells two milestones apart, so a new one may take the name and a restore beside it is a visible double, not a loss.

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 LockName = "milestones"

LockName keys the store lock every milestone write is made under. One name for the whole directory rather than one per file: creating a milestone reads every existing one to avoid their colours, so two creations at once have to be ordered even though they write different files.

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 CheckAfter added in v0.5.0

func CheckAfter(all []*Milestone, id string, tokens []string) error

CheckAfter refuses what the layer must never be handed: a predecessor no milestone on the board carries, and an arrow that would close a loop. The drawing ignores both rather than failing, so every write that places a milestone — the CLI's and the layer's — asks this first. id is the milestone being placed, and each token is an ID or a name as after: reads them.

func CheckFree added in v0.5.0

func CheckFree(root, name string) error

CheckFree asks the milestone directory whether a name can be given out: a milestone on the board under it is an *ExistsError, and nothing else holds a name. A filed one does not — its ID tells it from a new one — and neither does a milestone on a ref nobody has fetched here yet: the names of two people's milestones are told apart by ID, so a double is something to see and rename, not something to prevent across machines. Every door that hands out a name asks here: Rename, and the CLI's and the layer's create.

func CyclePath added in v0.5.0

func CyclePath(all []*Milestone, succ, pred string) []string

CyclePath names, by ID, the loop that making `pred` a predecessor of `succ` would close, in the direction the arrows run and closed on its first ID: for a→b→c and a new arrow c→a it is [a b c a]. Nil when there is no loop. Unknown tokens and unplaced milestones contribute no edges, as they do not when drawing.

func Dir

func Dir(root string) string

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

func FileIdentity added in v0.5.0

func FileIdentity(base string) (name, id string, idNamed bool)

FileIdentity is the name and ID a milestone file's base name (without .md) stands for, and whether the ID is in the name. A file written before IDs existed is its whole base name with the ID LegacyID computes for it.

func FileNamed added in v0.5.0

func FileNamed(name string) func(base string) bool

FileNamed is the test for a file base name (no .md) that belongs to a milestone called name, with or without an ID in it.

func IsID added in v0.5.0

func IsID(s string) bool

IsID reports whether s is a milestone ID: exactly six characters, upper case, from the ticket handle alphabet, at least one of them a letter.

func Labels added in v0.5.0

func Labels(all []*Milestone) map[string]string

Labels is how each of these milestones is called on screen, by ID: its name, and <name>-<ID> only for the ones that share a name with another — the one case in which a name does not tell them apart.

func LegacyID added in v0.5.0

func LegacyID(name string) string

LegacyID is the ID a milestone written before IDs existed is given: the first 30 bits of the SHA-256 of its old file name. It is computed rather than rolled so that every clone, offline and without talking to anybody, moves the same old file to the same ID. A hash of digits only is hashed again with a counter until a letter is in it.

func MigrateContent added in v0.5.0

func MigrateContent(content []byte) []byte

MigrateContent is a milestone file as the ID format wants it: every after: token that is not an ID becomes the ID LegacyID computes for that name, which is the ID the milestone it names gets when its own file moves. Nothing else is touched, and a file with nothing to change comes back byte for byte — two clones migrating the same file write the same bytes.

func NewID added in v0.5.0

func NewID() string

NewID rolls a fresh milestone ID: idLen random characters of the ticket handle alphabet. An ID of digits only is rolled again, because it could not be told from the tail of a name such as sprint-202611.

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 NoteConflict added in v0.5.0

func NoteConflict(content []byte, c Conflict) []byte

NoteConflict writes the comment for c into a milestone file, replacing the one an earlier clash on the same field left.

func ParseMember added in v0.5.1

func ParseMember(line string) (string, bool)

ParseMember reads one member line: "- <ticket id>", with anything after the id — a title, a comment — ignored and kept. Only a well-formed id counts, so an ordinary markdown bullet in the prose is not mistaken for a member. core/epic reads its member lines with this too, so the two lists cannot disagree about what a member line is.

func ParseNames added in v0.5.0

func ParseNames(raw string) ([]string, error)

ParseNames reads a comma-separated list of milestone names the way --after and the layer's form take it. Unlike a file being read, typed input is refused when a name cannot be one, rather than having it quietly left out. Empty is the empty list: a swimlane top.

func Pending added in v0.5.0

func Pending(root string) bool

Pending reports whether a file written before IDs existed lies in the milestone directory, which is when Migrate has something to do.

func ReattachSuccessors added in v0.5.0

func ReattachSuccessors(all []*Milestone, id string) map[string][]string

ReattachSuccessors computes what taking the milestone with this ID out of the layer does to the milestones that come after it: in each one's after list the token naming it is replaced by the removed milestone's own predecessors, order kept, repeats dropped, so the chain closes up instead of breaking. Every token that names a milestone comes back as that milestone's ID; one that names nothing is kept as written. A successor left with nothing becomes a swimlane top (an empty, non-nil list). Only the milestones that name it are in the result, keyed by ID, and writing it back is the caller's job.

func RenamedTo added in v0.5.0

func RenamedTo(content []byte) string

RenamedTo is the value of the `renamed-to:` line a rename of the name-keyed era left in the file it marked under the old name, or "" when the file has none. Such a file is not a milestone any more. It reads the raw lines — the field it was parsed into is gone.

func Resolve added in v0.5.0

func Resolve(all []*Milestone, token string) string

Resolve returns the ID an after: token names among these milestones — the token as an ID, or as the one milestone that carries it as a name — or "".

func ResolveRefs added in v0.5.0

func ResolveRefs(all []*Milestone, id string, tokens []string) ([]string, error)

ResolveRefs turns the predecessors a person typed for the milestone with this ID — each an ID, a <name>-<ID> or a name, as Load reads them — into the IDs after: stores, and puts them through CheckAfter. all is the milestones on the board. A token nobody carries is a *PredecessorError, a name two milestones carry an *AmbiguousError and an arrow that closes a loop CheckAfter's refusal, so a write that refuses has written nothing; repeats are dropped.

func SaveBase added in v0.5.0

func SaveBase(root, id string, base Base) error

SaveBase records the base for the milestone with this ID. Best effort for its callers: without a base the old side is simply not adopted.

func SpellAsID added in v0.5.0

func SpellAsID(all []*Milestone, token string) string

SpellAsID is SpellAsIDs for one token.

func SpellAsIDs added in v0.5.0

func SpellAsIDs(all []*Milestone, tokens []string) []string

SpellAsIDs gives every token that names exactly one milestone as that milestone's ID, and leaves the others as they are.

func SplitFile added in v0.5.0

func SplitFile(base string) (name, id string, ok bool)

SplitFile reads a milestone file's base name (without .md) as <name>-<ID>. ok is false for a file written before IDs existed, whose whole base name is the name.

func StaleOld added in v0.5.0

func StaleOld(rec Recorder, name string, content []byte) bool

StaleOld reports whether an old <name>.md holds a state the old name ref already carried. That is a copy a clone never updated, not an edit made here: it adopts nothing, and what the ref carries since the move is adopted from the ref.

Types

type AmbiguousError added in v0.5.0

type AmbiguousError struct {
	Name    string
	Choices []string
}

AmbiguousError is Load's answer when a name is carried by more than one milestone: nothing is chosen for the caller, who is handed the <name>-<ID> forms that tell the milestones apart.

func (*AmbiguousError) Error added in v0.5.0

func (e *AmbiguousError) Error() string

type Arrow added in v0.5.0

type Arrow struct {
	From, To string // IDs
	Extra    bool
}

Arrow runs from a predecessor to its successor. Extra is false for the parent arrow every non-top bubble has and true for every further predecessor — a join inside one lane, or an arrow across lanes.

type Base added in v0.5.0

type Base struct {
	Content string `json:"content"`
	OldSHA  string `json:"old-sha,omitempty"`
}

Base is the old side of a migrated milestone as it stood when it was last merged: the migrated content, and the tip of the old name ref when there was one. A later old-side state is compared with Content; OldSHA only spares the comparison when the ref has not moved.

func BaseFor added in v0.5.0

func BaseFor(root string, rec Recorder, id string) (Base, bool)

BaseFor is the base for the milestone with this ID: the one recorded here when this clone moved it or last merged an older build's edit, else the one the migration commit on the ID ref gives, so a clone that never moved it merges from the same state.

func LoadBase added in v0.5.0

func LoadBase(root, id string) (Base, bool)

LoadBase reads the base recorded for the milestone with this ID.

type Bubble added in v0.5.0

type Bubble struct {
	ID     string
	Lane   int
	Col    int
	Row    int
	Parent string // ID of the parent; "" for a lane top
}

Bubble is one placed milestone on the grid. Col is absolute across all lanes, Row is the depth below the lane's top, Lane indexes Grid.Lanes.

type Conflict added in v0.5.0

type Conflict struct {
	Milestone string `json:"milestone"`
	Field     string `json:"field"`
	Old       string `json:"old"`
}

Conflict is a field the older build and this one both changed since the base. This side is kept; Old is what the older build wrote, so it can be told rather than dropped.

func Adopt added in v0.5.0

func Adopt(name string, base, old, cur []byte, all []*Milestone) ([]byte, []Conflict)

Adopt merges the old side into the ID milestone. base is what the old side looked like when last merged, old what it is now, cur the ID milestone's file; all are the milestones on the board, for spelling names as IDs.

  • tickets added or removed on the old side since base are added to or removed from cur;
  • a frontmatter field (goal, after, any other) or the body that only the old side changed is taken; after: names are spelled as IDs;
  • one that both sides changed keeps cur, is returned as a Conflict, and is written into the file as a comment, because a fetch that runs in the background has nobody to tell.

The result equals cur byte for byte when there is nothing to adopt.

func AdoptOld added in v0.5.0

func AdoptOld(root string, rec Recorder, id, name string, old []byte, tip string) (adopted bool, conflicts []Conflict, err error)

AdoptOld merges what an older build wrote to its old file or old name ref (old, the whole file; tip, the ref's SHA when it came from one) into the ID file of the milestone with this ID, and queues the ref write. Without a base (BaseFor) the old side is merged against an empty one. It does nothing when the old side is what the base says, or when no ID file lies here. The base then becomes the old side as it is now, so the same edit is merged, and reported, once.

func (Conflict) String added in v0.5.0

func (c Conflict) String() string

type ExistsError added in v0.5.0

type ExistsError struct {
	Name string
	File string // the file of the one that has it; of two, the first by ID
}

ExistsError is CheckFree's answer for a name a milestone on the board already has.

func (*ExistsError) Error added in v0.5.0

func (e *ExistsError) Error() string

type FiledError added in v0.5.0

type FiledError struct {
	Name string
	File string // the file it lies in, which is what 'jaira restore' takes
}

FiledError is Edit's refusal of a milestone whose file still lies here but is marked filed — what a fetch of somebody else's filing leaves behind. Writing it would put the change on its ref for every clone that fetches.

func (*FiledError) Error added in v0.5.0

func (e *FiledError) Error() string

type Grid added in v0.5.0

type Grid struct {
	Bubbles []Bubble
	Lanes   []Lane
	Arrows  []Arrow
}

Grid is the layout: bubbles ordered by lane, row, column; lanes left to right; arrows ordered by their target.

func Layout added in v0.5.0

func Layout(all []*Milestone) Grid

Layout arranges the placed milestones.

  1. A milestone's parent is its first predecessor, when that is a placed milestone and the arrow closes no loop; otherwise it is a swimlane top.
  2. A top with everything under it is one swimlane; lanes run left to right by the top's age.
  3. Inside a lane the tree is tidy: row is depth, children are ordered by age, a subtree is as wide as its children together (at least one column), and a parent stands over its first child.
  4. Every further usable predecessor is an extra arrow.

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 milestone want names: by its ID, by its <name>-<ID>, or by its name — and a name hits every milestone that carries it, since filtering writes nothing and two milestones of one name are the same question. Letter case is not told apart. The comparison is equality, not a substring: a milestone is one of 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 Lane added in v0.5.0

type Lane struct {
	Top   string // ID of the lane's top
	Col   int
	Width int
}

Lane is one swimlane: the tree under one top, as a span of columns.

type Locker added in v0.5.0

type Locker interface {
	Lock(name string) (unlock func(), err error)
}

Locker is the one thing of the ticket store a write needs. An interface so this package does not import the store's locking and a test can count.

type Milestone

type Milestone struct {
	Name      string
	ID        string // the six characters that stay when the name changes
	Colour    int
	CreatedAt time.Time
	Status    string
	Goal      string   // free text, one line; empty when the file has none
	After     []string // predecessor IDs (a name only for a hand-written line), first one decides the swimlane
	Placed    bool     // an `after:` line exists; `after: []` is a swimlane top
	// 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 Create added in v0.5.0

func Create(root string, lock Locker, rec Recorder, build func(existing []*Milestone) (*Milestone, error)) (*Milestone, error)

Create is Edit's counterpart for a milestone that does not exist yet. build runs under the lock with every milestone on the board, because choosing a colour and checking the name is free are both reads that must not race a second creation; it returns the milestone to write, or refuses with an error and nothing is written.

func Edit added in v0.5.0

func Edit(root string, lock Locker, rec Recorder, name string, fn func(*Milestone) error) (*Milestone, error)

Edit is the one write path for a milestone that exists: lock, read, let fn change it, save atomically, record the ref. The CLI and the board both write through it so they cannot drift — what a person does in the layer is exactly what an agent does with the command.

fn may refuse by returning an error; nothing is written then. The file is saved only when fn changed its bytes, because a re-add of a ticket already there is not a write and must not queue a ref. A missing file comes back as the Load error (errors.Is os.ErrNotExist) so the caller can say why in its own words, and a filed one as a *FiledError before fn runs.

When fn takes a placed milestone out of the layer, Edit also closes the chain behind it under the same lock: every milestone that came after it now comes after what came before it (ReattachSuccessors). Here and not in the callers, so the layer's x and 'jaira milestone set --no-roadmap' cannot leave two different pictures behind.

func FromBytes

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

FromBytes reads a milestone out of bytes that are not on this disk — what a ref carries — under the file name the ref carries them as. 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, key string) (*Milestone, error)

Load reads one milestone, filed ones included, by whatever the caller has: its ID (any case), its <name>-<ID> file name, or a name. A name that two milestones carry is an *AmbiguousError, and a name on the board is preferred over the same name on a file that is filed. Nothing found reports os.ErrNotExist, so a caller can tell "no such milestone" from "unreadable".

The name and the ID come from the file name — the frontmatter does not carry either. 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 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, which is how a restore gets at it.

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 Rename added in v0.5.0

func Rename(root string, lock Locker, rec Recorder, from, to string) (*Milestone, error)

Rename gives the milestone, found by whatever names it, the name to. The ID stays, so the milestone keeps its ref and its place in every after: line: a rename is the file under its new name, the old file gone, and one write on the one ref. Nothing else is written, and nobody who follows the milestone is touched.

to has to be a name no milestone on the board carries (CheckFree). The milestone refuses the way Edit does when it is filed, and renaming it to its own name writes nothing.

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) FileName added in v0.5.0

func (m *Milestone) FileName() string

FileName is the file this milestone lives in under the milestone directory: the name it was read under, or <name>-<ID>.md for one never written.

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) Path added in v0.5.0

func (m *Milestone) Path(root string) string

Path is the file this milestone lives in.

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) SetAfter added in v0.5.0

func (m *Milestone) SetAfter(names []string)

SetAfter places the milestone in the layer under the given predecessors; no names makes it the top of a swimlane of its own (`after: []`). Names are normalized and deduplicated, so what is written is what a reader gets back.

func (*Milestone) SetColour added in v0.5.0

func (m *Milestone) SetColour(colour int)

SetColour writes the colour line where it stands — the layer's c, which steps a milestone through the palette.

func (*Milestone) SetGoal added in v0.5.0

func (m *Milestone) SetGoal(text string)

SetGoal writes, replaces or (empty text) removes the goal line. One line of text: a newline in it would end the value and leave the rest as a stray frontmatter line, so line breaks become spaces.

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.

func (*Milestone) Unplace added in v0.5.0

func (m *Milestone) Unplace()

Unplace takes the milestone out of the layer by removing its after line.

type PredecessorError added in v0.5.0

type PredecessorError struct {
	Name, Pred string
	Cycle      []string // nil when Pred is unknown
}

PredecessorError is CheckAfter's refusal: Pred names no milestone on the board, or — Cycle set — making it a predecessor of Name closes that loop. Cycle is spelled in names, for a person to read.

func (*PredecessorError) Error added in v0.5.0

func (e *PredecessorError) Error() string

type RecordError added in v0.5.0

type RecordError struct {
	Name string
	Err  error
}

RecordError reports that the file was saved and only the ref could not be written. The milestone is returned beside it: the write happened, so a caller that wants the old best-effort behaviour downgrades this to a warning and carries on.

func (*RecordError) Error added in v0.5.0

func (e *RecordError) Error() string

func (*RecordError) Unwrap added in v0.5.0

func (e *RecordError) Unwrap() error

type Recorder added in v0.5.0

type Recorder interface {
	RecordMilestone(key, file string, content []byte) error
	// HasMilestoneRef reports whether the milestone with this ID already has
	// its ref in this tree.
	HasMilestoneRef(id string) bool
}

Recorder queues a milestone file for its ref; refsync's Syncer is one. A nil *Syncer is a valid Recorder that does nothing, which is how a board with no remote stays a supported way to work.

type Result added in v0.5.0

type Result struct {
	Moved     []string
	Adopted   []string
	Conflicts []Conflict
}

Result is what Migrate did: the files it moved ("old.md -> new.md"), the milestones that took an edit an older build had made to their old file, and the fields where that edit clashed with this side's.

func Migrate added in v0.5.0

func Migrate(root string, rec Recorder) (Result, error)

Migrate moves every milestone file written before IDs existed to its <name>-<ID>.md name, once, with the ID LegacyID computes from the old name — the same on every clone, so clones that migrate without ever speaking to one another agree. The caller holds the milestone lock. It returns one "old.md -> new.md" per file it moved.

Each move records a Base, the old content and the tip of the old name ref, here and as the commit of its own the ref's first write is (RecordMigration). An old <name>.md that turns up beside its ID file later — an older build wrote it again — is not simply removed: what it changed since the base is adopted into the ID file (Adopt), and the ref write is queued. An old file that is the very file the ID file is (a case-insensitive file system) is left alone.

The new file is written before the old one is removed, so a crash leaves the milestone twice, never nowhere, and a second run finishes the move. A target that is already there is the newer file: only the old one goes. The mark a rename of the name-keyed era left under its old name is not a milestone and is removed. A migrated file whose ID has no ref in this tree is queued for it, so the move reaches the remote; the old name's ref stays where it is. A ref that cannot be queued does not stop the walk: the move is done, and only that milestone's own next write queues it, so the remaining files are moved too and the first such failure is returned, as a RecordError, after all of them.

type Twin added in v0.5.0

type Twin struct {
	Name   string
	Labels []string
}

Twin is one name that two or more milestones on the board share: the <name>-<ID> labels that tell them apart, in file order.

func Twins added in v0.5.0

func Twins(all []*Milestone) []Twin

Twins is every name these milestones share, in the order the names first appear. nil when no two milestones are called the same — the one rule every warning about twins reads, so the CLI and the board cannot disagree.

func (Twin) RenameCommand added in v0.5.0

func (t Twin) RenameCommand() string

RenameCommand is the command that ends the twinning, renaming the first.

Jump to

Keyboard shortcuts

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