Documentation
¶
Overview ¶
Package gitstore is aeman's storage backend: a board is a set of files in a git repository, every change is a commit. This file is the layout — where a card lives and what its file says. See docs/design/git-backend.md.
Index ¶
- Constants
- Variables
- func CardPath(id string) (string, error)
- func DeadlinePath(projectID, id string) string
- func DeriveID(t time.Time, namespace string, keys ...string) string
- func EncodeBoard(f BoardFile) ([]byte, error)
- func EncodeCard(f CardFile) ([]byte, error)
- func EncodeDeadline(f DeadlineFile) ([]byte, error)
- func EncodeEpic(f EpicFile) ([]byte, error)
- func EncodeProcess(f ProcessFile) ([]byte, error)
- func EncodeProject(f ProjectFile) ([]byte, error)
- func EncodeTeam(f TeamFile) ([]byte, error)
- func EncodeUser(f UserFile) ([]byte, error)
- func EpicPath(projectID, id string) string
- func IDTime(id string) (time.Time, error)
- func InitBoard(ctx context.Context, s storage.Storer, remote Remote, opts Options, ...) error
- func IterationID(task, week string) string
- func MigrateSchema(r *Repo) (bool, error)
- func MintID(in board.CreateInput, now time.Time) string
- func NewID(t time.Time) string
- func ProcessPath(id string) string
- func ProjectPath(id string) string
- func ScopeCards(ctx context.Context) []string
- func TaskPath(processID, id string) string
- func TeamPath(id string) string
- func UserPath(login string) string
- func WithScope(ctx context.Context, a Action) (context.Context, func() (plumbing.Hash, error))
- type Action
- type Alias
- type Backend
- func (b *Backend) AddNote(ctx context.Context, _ board.Board, card board.Card, text string) error
- func (b *Backend) AppendEvent(ctx context.Context, _ board.Board, card board.Card, e board.Event) error
- func (b *Backend) CardLog(_ context.Context, _ board.Board, id string) ([]board.Event, time.Time, error)
- func (b *Backend) CardLogSince(_ context.Context, _ board.Board, id string, since time.Time) ([]board.Event, time.Time, error)
- func (b *Backend) CreateCard(ctx context.Context, _ board.Board, in board.CreateInput) (board.Card, error)
- func (b *Backend) DeleteCard(ctx context.Context, _ board.Board, card board.Card) error
- func (b *Backend) DeleteNote(ctx context.Context, _ board.Board, card board.Card, note board.Note) error
- func (b *Backend) EditNote(ctx context.Context, _ board.Board, card board.Card, note board.Note, ...) error
- func (b *Backend) LoadBoard(_ context.Context, boardID string) (board.Board, error)
- func (b *Backend) LoadBoardAsOf(_ context.Context, boardID string, at time.Time) (board.Board, bool, error)
- func (b *Backend) LoadCards(ctx context.Context, _ board.Board, ids []string) ([]board.Card, error)
- func (b *Backend) MoveCard(ctx context.Context, _ board.Board, card board.Card, afterID string) error
- func (b *Backend) RenameCard(ctx context.Context, _ board.Board, card board.Card, title string) error
- func (b *Backend) SetAccumulate(ctx context.Context, _ board.Board, card board.Card, on bool) error
- func (b *Backend) SetAssignee(ctx context.Context, _ board.Board, card board.Card, login string) error
- func (b *Backend) SetBacklog(ctx context.Context, _ board.Board, card board.Card, parked bool) error
- func (b *Backend) SetDay(ctx context.Context, _ board.Board, card board.Card, day string) error
- func (b *Backend) SetDescription(ctx context.Context, _ board.Board, card board.Card, description string) error
- func (b *Backend) SetDoneAt(ctx context.Context, _ board.Board, card board.Card, day string) error
- func (b *Backend) SetEpic(ctx context.Context, _ board.Board, card board.Card, epic string) error
- func (b *Backend) SetLeftAt(ctx context.Context, _ board.Board, card board.Card, day string) error
- func (b *Backend) SetMirrors(ctx context.Context, _ board.Board, card board.Card, mirrors []board.Placement) error
- func (b *Backend) SetParent(ctx context.Context, _ board.Board, card board.Card, parent string) error
- func (b *Backend) SetPaused(ctx context.Context, _ board.Board, card board.Card, paused bool) error
- func (b *Backend) SetPersonCapacity(ctx context.Context, _ board.Board, login string, points int) error
- func (b *Backend) SetProcess(ctx context.Context, _ board.Board, card board.Card, process string) error
- func (b *Backend) SetProgress(ctx context.Context, _ board.Board, card board.Card, progress int) error
- func (b *Backend) SetProject(ctx context.Context, _ board.Board, card board.Card, project string) error
- func (b *Backend) SetRecurrence(ctx context.Context, _ board.Board, card board.Card, cycle string) error
- func (b *Backend) SetReviewOf(ctx context.Context, _ board.Board, card board.Card, reviewOf string) error
- func (b *Backend) SetReviewRound(ctx context.Context, _ board.Board, card board.Card, round int) error
- func (b *Backend) SetSize(ctx context.Context, _ board.Board, card board.Card, size board.SizeKey) error
- func (b *Backend) SetSprintStart(ctx context.Context, _ board.Board, card board.Card, date string) error
- func (b *Backend) SetSprintState(ctx context.Context, _ board.Board, team, current, previous string) error
- func (b *Backend) SetStage(ctx context.Context, _ board.Board, card board.Card, stage board.StageKey) error
- func (b *Backend) SetStart(ctx context.Context, _ board.Board, card board.Card, date string) error
- func (b *Backend) SetTask(ctx context.Context, _ board.Board, card board.Card, task string) error
- func (b *Backend) SetTeam(ctx context.Context, _ board.Board, card board.Card, team string) error
- func (b *Backend) SetTeamPoints(ctx context.Context, _ board.Board, team string, points int) error
- func (b *Backend) SetWeek(ctx context.Context, _ board.Board, card board.Card, week string) error
- func (b *Backend) SetZone(ctx context.Context, _ board.Board, card board.Card, zone board.ZoneKey) error
- type BackendOptions
- type BoardFile
- type BrokenFile
- type CardFile
- type Change
- type Deadline
- type DeadlineFile
- type Domain
- type Epic
- type EpicFile
- type ExtraField
- type FileWrite
- type Ghost
- type Identity
- type Log
- type LogEntry
- type MultiBackend
- func (mb *MultiBackend) AddDomain(d Domain) error
- func (mb *MultiBackend) AddNote(ctx context.Context, bd board.Board, card board.Card, text string) error
- func (mb *MultiBackend) AppendEvent(ctx context.Context, bd board.Board, card board.Card, e board.Event) error
- func (mb *MultiBackend) CardLog(ctx context.Context, bd board.Board, id string) ([]board.Event, time.Time, error)
- func (mb *MultiBackend) CardLogSince(ctx context.Context, bd board.Board, id string, since time.Time) ([]board.Event, time.Time, error)
- func (mb *MultiBackend) CreateCard(ctx context.Context, bd board.Board, in board.CreateInput) (board.Card, error)
- func (mb *MultiBackend) DeleteCard(ctx context.Context, bd board.Board, card board.Card) error
- func (mb *MultiBackend) DeleteNote(ctx context.Context, bd board.Board, card board.Card, note board.Note) error
- func (mb *MultiBackend) Domains() []Domain
- func (mb *MultiBackend) EditNote(ctx context.Context, bd board.Board, card board.Card, note board.Note, ...) error
- func (mb *MultiBackend) Issues() ([]Alias, []Ghost)
- func (mb *MultiBackend) LoadBoard(_ context.Context, boardID string) (board.Board, error)
- func (mb *MultiBackend) LoadBoardAsOf(_ context.Context, boardID string, at time.Time) (board.Board, bool, error)
- func (mb *MultiBackend) LoadBoardOfDay(_ context.Context, boardID string, from, to time.Time) (board.Board, bool, error)
- func (mb *MultiBackend) LoadCards(ctx context.Context, bd board.Board, ids []string) ([]board.Card, error)
- func (mb *MultiBackend) MoveCard(ctx context.Context, bd board.Board, card board.Card, afterID string) error
- func (mb *MultiBackend) RemoveDomain(name string) error
- func (mb *MultiBackend) RenameCard(ctx context.Context, bd board.Board, card board.Card, title string) error
- func (mb *MultiBackend) SetAccumulate(ctx context.Context, bd board.Board, card board.Card, on bool) error
- func (mb *MultiBackend) SetAssignee(ctx context.Context, bd board.Board, card board.Card, login string) error
- func (mb *MultiBackend) SetBacklog(ctx context.Context, bd board.Board, card board.Card, parked bool) error
- func (mb *MultiBackend) SetDay(ctx context.Context, bd board.Board, card board.Card, day string) error
- func (mb *MultiBackend) SetDescription(ctx context.Context, bd board.Board, card board.Card, description string) error
- func (mb *MultiBackend) SetDoneAt(ctx context.Context, bd board.Board, card board.Card, day string) error
- func (mb *MultiBackend) SetEpic(ctx context.Context, bd board.Board, card board.Card, epic string) error
- func (mb *MultiBackend) SetLeftAt(ctx context.Context, bd board.Board, card board.Card, day string) error
- func (mb *MultiBackend) SetMirrors(ctx context.Context, bd board.Board, card board.Card, ...) error
- func (mb *MultiBackend) SetParent(ctx context.Context, bd board.Board, card board.Card, parent string) error
- func (mb *MultiBackend) SetPaused(ctx context.Context, bd board.Board, card board.Card, paused bool) error
- func (mb *MultiBackend) SetPersonCapacity(ctx context.Context, bd board.Board, login string, points int) error
- func (mb *MultiBackend) SetProcess(ctx context.Context, bd board.Board, card board.Card, process string) error
- func (mb *MultiBackend) SetProgress(ctx context.Context, bd board.Board, card board.Card, progress int) error
- func (mb *MultiBackend) SetProject(ctx context.Context, bd board.Board, card board.Card, project string) error
- func (mb *MultiBackend) SetRecurrence(ctx context.Context, bd board.Board, card board.Card, cycle string) error
- func (mb *MultiBackend) SetReviewOf(ctx context.Context, bd board.Board, card board.Card, reviewOf string) error
- func (mb *MultiBackend) SetReviewRound(ctx context.Context, bd board.Board, card board.Card, round int) error
- func (mb *MultiBackend) SetSize(ctx context.Context, bd board.Board, card board.Card, size board.SizeKey) error
- func (mb *MultiBackend) SetSprintStart(ctx context.Context, bd board.Board, card board.Card, date string) error
- func (mb *MultiBackend) SetSprintState(ctx context.Context, bd board.Board, team, current, previous string) error
- func (mb *MultiBackend) SetStage(ctx context.Context, bd board.Board, card board.Card, stage board.StageKey) error
- func (mb *MultiBackend) SetStart(ctx context.Context, bd board.Board, card board.Card, date string) error
- func (mb *MultiBackend) SetTask(ctx context.Context, bd board.Board, card board.Card, task string) error
- func (mb *MultiBackend) SetTeam(ctx context.Context, bd board.Board, card board.Card, team string) error
- func (mb *MultiBackend) SetTeamPoints(ctx context.Context, bd board.Board, team string, points int) error
- func (mb *MultiBackend) SetWeek(ctx context.Context, bd board.Board, card board.Card, week string) error
- func (mb *MultiBackend) SetZone(ctx context.Context, bd board.Board, card board.Card, zone board.ZoneKey) error
- func (mb *MultiBackend) SweepGhosts(_ context.Context, landed func(domain string) bool) (int, error)
- func (mb *MultiBackend) Users() []User
- type Options
- type PathKind
- type Process
- type ProcessFile
- type Project
- type ProjectFile
- type RebaseResult
- type Remote
- type Repo
- func (r *Repo) Branch() plumbing.ReferenceName
- func (r *Repo) CardLog(id string, limit int) (Log, error)
- func (r *Repo) CardLogSince(id string, since time.Time) (Log, error)
- func (r *Repo) ChangedPaths(from, to plumbing.Hash) ([]string, error)
- func (r *Repo) Commit(a Action, writes []FileWrite) (plumbing.Hash, error)
- func (r *Repo) CommitObject(h plumbing.Hash) (*object.Commit, error)
- func (r *Repo) DeepenSince(ctx context.Context, remote Remote, since time.Time) error
- func (r *Repo) Fetch(ctx context.Context, remote Remote) (plumbing.Hash, bool, error)
- func (r *Repo) Head() plumbing.Hash
- func (r *Repo) Maintain() error
- func (r *Repo) Push(ctx context.Context, remote Remote) error
- func (r *Repo) PushForce(ctx context.Context, remote Remote) error
- func (r *Repo) ReadFile(p string) ([]byte, error)
- func (r *Repo) Rebase(tip plumbing.Hash) (RebaseResult, error)
- func (r *Repo) RemoteTip() plumbing.Hash
- func (r *Repo) ResetTo(h plumbing.Hash) error
- func (r *Repo) Storer() storage.Storer
- func (r *Repo) Unpushed() (int, error)
- func (r *Repo) UnpushedCommits() ([]*object.Commit, error)
- func (r *Repo) Walk(from plumbing.Hash, fn func(*object.Commit) (bool, error)) error
- type Snapshot
- func Load(r *Repo) (Snapshot, error)
- func LoadAll(domains []Domain) (Snapshot, error)
- func LoadAllAsOf(domains []Domain, at time.Time) (Snapshot, bool, error)
- func LoadAllAsOfDay(domains []Domain, from, to time.Time) (Snapshot, bool, error)
- func LoadAsOf(r *Repo, at time.Time) (Snapshot, bool, error)
- func LoadAsOfDay(r *Repo, from, to time.Time) (Snapshot, bool, error)
- func LoadAt(r *Repo, h plumbing.Hash) (Snapshot, error)
- type SprintPointer
- type Task
- type Team
- type TeamFile
- type Trailers
- type User
- type UserFile
Constants ¶
const BoardPath = "board.yaml"
BoardPath is the board's own file, primary domain only.
const SchemaVersion = 1
SchemaVersion is the layout version this server writes and understands.
Variables ¶
var ( ErrProjectNotFound = errors.New("gitstore: project not found") ErrProcessNotFound = errors.New("gitstore: process not found") ErrNoteNotFound = errors.New("gitstore: note not found") ErrCardNotFound = errors.New("gitstore: card not found") )
Errors the backend reports for the roster protocol.
var ErrBadID = errors.New("gitstore: id too short to shard")
ErrBadID is returned for an id that cannot be placed in the layout.
var ErrBadIDFormat = errors.New("gitstore: not a ulid")
ErrBadIDFormat is returned for a string that is not a ULID.
var ErrEmptyRepository = errors.New("gitstore: repository has no commits")
ErrEmptyRepository is returned when the branch has no commits: a board that was never initialised, which `serve` reports as "run aeman init" rather than serving an empty board.
var ErrNameTaken = errors.New("gitstore: name already declared on the board")
ErrNameTaken refuses a roster name — a team, a project, a process — that some domain of the board already declares. Names are one namespace across the board: a card refers to its team and project by name, and the domain rule resolves them by name, so two of a kind would leave a card's home undecidable. The store checks every domain, readable by the caller or not; the read-side alias merge is left for collisions made behind its back.
var ErrNoDomains = errors.New("gitstore: a board needs at least its primary domain")
ErrNoDomains is returned when a board has nothing to read.
var ErrNoRemoteTip = errors.New("gitstore: remote has no such branch")
ErrNoRemoteTip is returned when the remote has no branch to sync with.
var ErrNotFound = errors.New("gitstore: not found")
ErrNotFound is returned by readers for a missing path.
var ErrPersonalRoster = fmt.Errorf("gitstore: a personal board holds no teams, projects or processes")
ErrPersonalRoster refuses a team, project or process in a personal domain: a personal board holds cards and nothing else — a roster entry there would sit in a repository nobody else can read, on a board with no place to show it.
var ErrSchemaNewer = errors.New("gitstore: repository schema is newer than this server")
ErrSchemaNewer is returned when a repository was written by a newer server: refusing is safer than misreading it.
var ErrUnknownDomain = fmt.Errorf("gitstore: unknown domain")
ErrUnknownDomain names a domain that is not part of the board.
Functions ¶
func CardPath ¶
CardPath is where a card lives: cards/<a>/<b>/<id>.md, with a and b the id's LAST two characters, one per level. Every commit rewrites the tree of each directory on the changed file's path, so the leaves stay tiny; the tail rather than the head because a ULID's head is a timestamp. The path carries identity only — no state ever moves a file.
func DeadlinePath ¶
DeadlinePath is one deadline line of a project.
func DeriveID ¶
DeriveID mints the id that (namespace, keys…) always maps to at the given moment: the tail is a hash of the inputs instead of random bits, so the migration gets byte-identical re-runs and two replicas filing the same iteration write the same path.
func EncodeBoard ¶
EncodeBoard renders board.yaml. The schema is always written, even at zero, because its absence means "older than versioning".
func EncodeCard ¶
EncodeCard renders a card file: front-matter, the description, the notes. Empty fields are omitted — the file says what is, not what is not — and derived states (In Progress, done-by-100%) are never written because they are never in the card to begin with.
func EncodeDeadline ¶
func EncodeDeadline(f DeadlineFile) ([]byte, error)
EncodeDeadline renders a deadline file.
func EncodeProcess ¶
func EncodeProcess(f ProcessFile) ([]byte, error)
EncodeProcess renders a process file.
func EncodeProject ¶
func EncodeProject(f ProjectFile) ([]byte, error)
EncodeProject renders a project file.
func InitBoard ¶
func InitBoard(ctx context.Context, s storage.Storer, remote Remote, opts Options, title string) error
InitBoard bootstraps an empty remote: board.yaml and the no-team group in one commit, pushed. A remote that already holds a board is left exactly as it is — init is safe to run twice — and a remote that holds something that is not a board is refused rather than written over.
func IterationID ¶
IterationID is the deterministic id of a process turn: a ULID whose time is the due week's Monday and whose random bits hash the task and the week. Two replicas sweeping the same due task write the SAME path, so the loser's create is a no-op on re-apply (G11) and one turn exists.
func MigrateSchema ¶
MigrateSchema brings board.yaml to SchemaVersion when it is older (or missing), in one "schema" commit, and reports whether it did. A current repository is left alone; a newer one is refused.
func MintID ¶
func MintID(in board.CreateInput, now time.Time) string
MintID is the id a create gets when the caller brings none: a turn's deterministic id, a fresh one for everything else. The cache and the backend both mint this way, so the id handed out before the write lands is the id the file gets.
func ScopeCards ¶
ScopeCards lists the cards the action has touched so far, in id order — after flush, exactly what its commits name in Aeman-Cards (a move's cascade included). Nil without a scope.
func UserPath ¶
UserPath is users/<login>.yaml — a person's link to their personal repository, read from the primary only.
func WithScope ¶
WithScope opens an action on the context: every backend write made with the returned context is staged, and flush commits them — one commit per repository touched, all carrying the action's trailers. A flush with nothing staged makes no commit and returns the zero hash; with several repositories it returns the last commit's hash.
Types ¶
type Action ¶
type Action struct {
// Name is the action kind — today's event kinds plus the actions that
// never had one (carry-over, rename-epic, import, sweep, …).
Name string
// ID ties the commits of one action together across domains; empty
// for single-domain actions that need no correlation.
ID string
// Actor is the login; empty means the server acted on its own.
Actor string
// Summary is the human first line of the message.
Summary string
// At is the action time — the commit date. Zero means now.
At time.Time
// Cards are the ids the action touched.
Cards []string
// Changes carry payloads that are not field diffs.
Changes []Change
// Trailers are extra "Aeman-Key: value" lines a caller adds — the
// migration marks its reconcile commit this way. Keys are written
// verbatim and must start with "Aeman-".
Trailers map[string]string
// AllowEmpty makes a commit even when no file changed: an annotation —
// the migration records an event whose payload is not a field. Live
// actions never set it; a no-op stays a no-op.
AllowEmpty bool
}
Action is what one commit records.
type Alias ¶
type Alias struct {
Kind string // team | project | process
Name string
Domain string
ID string
Winner string // the id of the entry that won
}
Alias is a roster entry that lost the duplicate-name resolution.
type Backend ¶
type Backend struct {
// contains filtered or unexported fields
}
Backend implements boardservice.Backend.
func NewBackend ¶
func NewBackend(repo *Repo, opts BackendOptions) *Backend
NewBackend wraps a repository.
func (*Backend) AppendEvent ¶
func (b *Backend) AppendEvent(ctx context.Context, _ board.Board, card board.Card, e board.Event) error
AppendEvent has no file to write: a commit is the event. Inside a scope the event becomes an Aeman-Change trailer — the way a payload that is not a field diff (a reviewer, a subtask's title) reaches the log. Outside a scope the field diff already says it, and this is a no-op.
func (*Backend) CardLog ¶
func (b *Backend) CardLog(_ context.Context, _ board.Board, id string) ([]board.Event, time.Time, error)
CardLog is the card's feed read from this repository's commits — each field change an event with the commit's actor and time — and the horizon a shallow clone cuts it at (zero when whole). MultiBackend.CardLog is the cross-domain version that follows a move.
func (*Backend) CardLogSince ¶ added in v0.26.6
func (b *Backend) CardLogSince(_ context.Context, _ board.Board, id string, since time.Time) ([]board.Event, time.Time, error)
CardLogSince is CardLog cut at a boundary — the day feed's read.
func (*Backend) CreateCard ¶
func (b *Backend) CreateCard(ctx context.Context, _ board.Board, in board.CreateInput) (board.Card, error)
CreateCard creates a card — or, for a marker title, the roster entry the stub stands for — and returns it with its new id.
func (*Backend) DeleteCard ¶
DeleteCard removes a card's file, or a stub's roster file.
func (*Backend) DeleteNote ¶
func (b *Backend) DeleteNote(ctx context.Context, _ board.Board, card board.Card, note board.Note) error
DeleteNote removes one note.
func (*Backend) EditNote ¶
func (b *Backend) EditNote(ctx context.Context, _ board.Board, card board.Card, note board.Note, text string) error
EditNote rewrites one note's text.
func (*Backend) LoadBoard ¶
LoadBoard reads the domain and hands the service the shape it expects: the roster files become the state cards NewBoard splits back out, so the duplicate and ordering rules stay in one place.
func (*Backend) LoadBoardAsOf ¶ added in v0.30.0
func (b *Backend) LoadBoardAsOf(_ context.Context, boardID string, at time.Time) (board.Board, bool, error)
LoadBoardAsOf is LoadBoard at a past moment — the tree this domain had when that day ended (see LoadAsOf). ok is false behind the horizon.
func (*Backend) LoadCards ¶
LoadCards reads the asked-for cards, in the order asked; missing ones are omitted.
func (*Backend) MoveCard ¶
func (b *Backend) MoveCard(ctx context.Context, _ board.Board, card board.Card, afterID string) error
MoveCard gives the card (or stub) a rank between afterID and its successor in its own list — one file rewritten, nothing renumbered.
func (*Backend) RenameCard ¶
func (b *Backend) RenameCard(ctx context.Context, _ board.Board, card board.Card, title string) error
RenameCard changes the title.
func (*Backend) SetAccumulate ¶
SetAccumulate sets a task's accumulate flag.
func (*Backend) SetAssignee ¶
func (b *Backend) SetAssignee(ctx context.Context, _ board.Board, card board.Card, login string) error
SetAssignee sets the single assignee ("" clears).
func (*Backend) SetBacklog ¶ added in v0.34.0
func (b *Backend) SetBacklog( ctx context.Context, _ board.Board, card board.Card, parked bool, ) error
SetBacklog puts the card on its team's shelf, or takes it off.
func (*Backend) SetDescription ¶
func (b *Backend) SetDescription(ctx context.Context, _ board.Board, card board.Card, description string) error
SetDescription replaces the free-form description.
func (*Backend) SetDoneAt ¶ added in v0.35.0
SetDoneAt writes the board day the card counts as finished on, without touching the progress that normally sets it.
func (*Backend) SetMirrors ¶ added in v0.28.0
func (b *Backend) SetMirrors(ctx context.Context, _ board.Board, card board.Card, mirrors []board.Placement) error
SetMirrors replaces the card's mirror placements — the additional Project-board columns the same card stands in.
func (*Backend) SetParent ¶
func (b *Backend) SetParent(ctx context.Context, _ board.Board, card board.Card, parent string) error
SetParent sets or clears the parent link.
func (*Backend) SetPersonCapacity ¶ added in v0.37.0
func (b *Backend) SetPersonCapacity(ctx context.Context, _ board.Board, login string, points int) error
SetPersonCapacity records the points a week a lead set for a person (0 takes it back: the board then has no number for them at all).
func (*Backend) SetProcess ¶
func (b *Backend) SetProcess(ctx context.Context, _ board.Board, card board.Card, process string) error
SetProcess names a process on a stub (rename) or a task (its process).
func (*Backend) SetProgress ¶
func (b *Backend) SetProgress(ctx context.Context, _ board.Board, card board.Card, progress int) error
SetProgress sets the readiness. Reaching 100 remembers where the card came from (doneFrom) and the board day it happened (doneAt); dropping below 100 forgets both — that is what a reopen is.
func (*Backend) SetProject ¶
func (b *Backend) SetProject(ctx context.Context, _ board.Board, card board.Card, project string) error
SetProject renames a project stub, re-parents an epic stub, sets a process's project, or files a card under a project.
func (*Backend) SetRecurrence ¶
func (b *Backend) SetRecurrence(ctx context.Context, _ board.Board, card board.Card, cycle string) error
SetRecurrence sets a recurrent card's cycle.
func (*Backend) SetReviewOf ¶
func (b *Backend) SetReviewOf(ctx context.Context, _ board.Board, card board.Card, reviewOf string) error
SetReviewOf sets or clears the review link.
func (*Backend) SetReviewRound ¶
func (b *Backend) SetReviewRound(ctx context.Context, _ board.Board, card board.Card, round int) error
SetReviewRound sets the review round.
func (*Backend) SetSize ¶ added in v0.37.0
func (b *Backend) SetSize(ctx context.Context, _ board.Board, card board.Card, size board.SizeKey) error
SetSize sets or clears the size.
func (*Backend) SetSprintStart ¶
func (b *Backend) SetSprintStart(ctx context.Context, _ board.Board, card board.Card, date string) error
SetSprintStart sets or clears the sprint membership.
func (*Backend) SetSprintState ¶
func (*Backend) SetStage ¶
func (b *Backend) SetStage(ctx context.Context, _ board.Board, card board.Card, stage board.StageKey) error
SetStage sets or clears the explicit stage.
func (*Backend) SetTeamPoints ¶ added in v0.37.0
SetTeamPoints records the points a week a lead set for a team, in the team's own file (0 takes it back). The team must exist: a capacity is said about something already declared.
type BackendOptions ¶
type BackendOptions struct {
// Now is the clock (tests pin it); nil means time.Now.
Now func() time.Time
}
BackendOptions configures a Backend.
type BoardFile ¶
type BoardFile struct {
Schema int
Title string
Extra []ExtraField
}
BoardFile is board.yaml.
func DecodeBoard ¶
DecodeBoard parses board.yaml. A schema newer than SchemaVersion is refused; an older or missing one comes back as is, for migration.
type BrokenFile ¶
BrokenFile is a layout path whose content could not be read.
type CardFile ¶
type CardFile struct {
Card board.Card
Extra []ExtraField
}
CardFile is one card's file: the card, plus front-matter keys this server does not know and carries along unchanged — a newer server must not lose what an older one wrote, nor the other way round.
type Change ¶
Change is one Aeman-Change trailer: a change on a card whose from/to cannot be read from the file's diff.
type Deadline ¶
type Deadline struct {
ID string
Domain string
DeadlineFile
}
Deadline is one deadline line.
type DeadlineFile ¶
type DeadlineFile struct {
Week string
Created string
Extra []ExtraField
}
DeadlineFile is projects/<pid>/deadlines/<id>.yaml.
func DecodeDeadline ¶
func DecodeDeadline(data []byte) (DeadlineFile, error)
DecodeDeadline parses a deadline file.
type EpicFile ¶
type EpicFile struct {
Name string
Rank string
Created string
Extra []ExtraField
}
EpicFile is projects/<pid>/epics/<id>.yaml.
type ExtraField ¶
ExtraField is an unknown front-matter key, kept in first-seen order.
type Ghost ¶
Ghost is the stale copy of a card caught between two domains: Domain is where the stale file sits, Current where the served copy is. Current is empty when neither copy says it moved — a duplicate, not a torn move, and not maintenance's to resolve.
type Log ¶
type Log struct {
Entries []LogEntry
// TruncatedBefore is the boundary's time when the history is cut by a
// shallow clone; zero when the whole history is present.
TruncatedBefore time.Time
}
Log is a card's history within the loaded horizon.
type LogEntry ¶
type LogEntry struct {
Hash plumbing.Hash
At time.Time
Actor string
Action string
ActionID string
Summary string
// Changes: the card's Aeman-Change trailers first, then what the
// front-matter diff says, then "description"/"note" for body edits. A
// commit that brings the file into being is one "created" change, one
// that removes it one "deleted".
Changes []Change
// MovedFrom names the domain the card came from when this commit is the
// destination half of a move (G22): the log continues there.
MovedFrom string
}
LogEntry is one commit as seen from one card.
type MultiBackend ¶
type MultiBackend struct {
// contains filtered or unexported fields
}
MultiBackend implements boardservice.Backend.
func NewMultiBackend ¶
func NewMultiBackend(domains []Domain, opts BackendOptions) *MultiBackend
NewMultiBackend builds the backend; domains[0] is the primary.
func (*MultiBackend) AddDomain ¶
func (mb *MultiBackend) AddDomain(d Domain) error
AddDomain attaches a domain at run time — a personal repository, attached when its owner first arrives. A name the board already has is refused.
func (*MultiBackend) AddNote ¶
func (mb *MultiBackend) AddNote(ctx context.Context, bd board.Board, card board.Card, text string) error
AddNote appends a note in the card's domain.
func (*MultiBackend) AppendEvent ¶
func (mb *MultiBackend) AppendEvent(ctx context.Context, bd board.Board, card board.Card, e board.Event) error
AppendEvent records the event on the card's domain commit.
func (*MultiBackend) CardLog ¶
func (mb *MultiBackend) CardLog(ctx context.Context, bd board.Board, id string) ([]board.Event, time.Time, error)
CardLog is the card's feed read from the commits: in the domain it lives in and, following Aeman-Moved-From, in the domain it came from — one continuous list, newest first, each field change an event with the commit's actor and time. truncatedBefore is the horizon of the oldest domain walked when a shallow clone cuts the history; zero otherwise.
func (*MultiBackend) CardLogSince ¶ added in v0.26.6
func (mb *MultiBackend) CardLogSince(ctx context.Context, bd board.Board, id string, since time.Time) ([]board.Event, time.Time, error)
CardLogSince is CardLog cut at a boundary — the day feed's read, which follows a move the same way.
func (*MultiBackend) CreateCard ¶
func (mb *MultiBackend) CreateCard(ctx context.Context, bd board.Board, in board.CreateInput) (board.Card, error)
CreateCard places a new card by the inheritance rule, a roster stub where the caller asks (default the primary) — except that a column or deadline lives with its project, a task with its process, a process with its project when it has one.
func (*MultiBackend) DeleteCard ¶
DeleteCard removes the card from its domain.
func (*MultiBackend) DeleteNote ¶
func (mb *MultiBackend) DeleteNote(ctx context.Context, bd board.Board, card board.Card, note board.Note) error
DeleteNote removes a note in the card's domain.
func (*MultiBackend) Domains ¶
func (mb *MultiBackend) Domains() []Domain
Domains lists the board's domains, primary first.
func (*MultiBackend) EditNote ¶
func (mb *MultiBackend) EditNote(ctx context.Context, bd board.Board, card board.Card, note board.Note, text string) error
EditNote rewrites a note in the card's domain.
func (*MultiBackend) Issues ¶
func (mb *MultiBackend) Issues() ([]Alias, []Ghost)
Issues reports what the last load had to resolve: duplicate roster names (G13) and torn-move ghosts (G22). Nil before any load.
func (*MultiBackend) LoadBoardAsOf ¶ added in v0.30.0
func (mb *MultiBackend) LoadBoardAsOf(_ context.Context, boardID string, at time.Time) (board.Board, bool, error)
LoadBoardAsOf is LoadBoard at a past moment: the board every domain had when that day ended. ok is false when the clone's history does not reach back that far — the caller deepens and asks again, or tells the reader the history is cut.
func (*MultiBackend) LoadBoardOfDay ¶ added in v0.31.0
func (mb *MultiBackend) LoadBoardOfDay(_ context.Context, boardID string, from, to time.Time) (board.Board, bool, error)
LoadBoardOfDay is LoadBoardAsOf for a DAY: the board every domain ended it with, plus what the day itself removed (boardservice.DayReader).
func (*MultiBackend) LoadCards ¶
func (mb *MultiBackend) LoadCards(ctx context.Context, bd board.Board, ids []string) ([]board.Card, error)
LoadCards reads cards by id from whichever domain holds them.
func (*MultiBackend) MoveCard ¶
func (mb *MultiBackend) MoveCard(ctx context.Context, bd board.Board, card board.Card, afterID string) error
MoveCard reorders within the card's domain.
func (*MultiBackend) RemoveDomain ¶
func (mb *MultiBackend) RemoveDomain(name string) error
RemoveDomain detaches a domain; its cards are no longer served. The primary stays.
func (*MultiBackend) RenameCard ¶
func (mb *MultiBackend) RenameCard(ctx context.Context, bd board.Board, card board.Card, title string) error
RenameCard writes in the card's domain.
func (*MultiBackend) SetAccumulate ¶
func (mb *MultiBackend) SetAccumulate(ctx context.Context, bd board.Board, card board.Card, on bool) error
SetAccumulate writes in the task's domain.
func (*MultiBackend) SetAssignee ¶
func (mb *MultiBackend) SetAssignee(ctx context.Context, bd board.Board, card board.Card, login string) error
SetAssignee writes in the card's domain.
func (*MultiBackend) SetBacklog ¶ added in v0.34.0
func (mb *MultiBackend) SetBacklog( ctx context.Context, bd board.Board, card board.Card, parked bool, ) error
SetBacklog writes in the card's own domain: a list is the team's, and the card is already in the repository its team names.
func (*MultiBackend) SetDay ¶
func (mb *MultiBackend) SetDay(ctx context.Context, bd board.Board, card board.Card, day string) error
SetDay writes in the card's domain.
func (*MultiBackend) SetDescription ¶
func (mb *MultiBackend) SetDescription(ctx context.Context, bd board.Board, card board.Card, description string) error
SetDescription writes in the card's domain.
func (*MultiBackend) SetDoneAt ¶ added in v0.35.0
func (mb *MultiBackend) SetDoneAt(ctx context.Context, bd board.Board, card board.Card, day string) error
SetLeftAt writes in the card's domain. SetDoneAt writes in the card's domain.
func (*MultiBackend) SetEpic ¶
func (mb *MultiBackend) SetEpic(ctx context.Context, bd board.Board, card board.Card, epic string) error
SetEpic may move a card into another project's domain.
func (*MultiBackend) SetMirrors ¶ added in v0.28.0
func (mb *MultiBackend) SetMirrors(ctx context.Context, bd board.Board, card board.Card, mirrors []board.Placement) error
SetMirrors writes in the card's own domain: mirrors never move a card (same-repository by the service's guard), so no refile is involved.
func (*MultiBackend) SetParent ¶
func (mb *MultiBackend) SetParent(ctx context.Context, bd board.Board, card board.Card, parent string) error
SetParent may move a subtask to its parent's domain.
func (*MultiBackend) SetPaused ¶
func (mb *MultiBackend) SetPaused(ctx context.Context, bd board.Board, card board.Card, paused bool) error
SetPaused writes in the process's domain.
func (*MultiBackend) SetPersonCapacity ¶ added in v0.37.0
func (mb *MultiBackend) SetPersonCapacity(ctx context.Context, bd board.Board, login string, points int) error
SetPersonCapacity writes in the PRIMARY: people are the board's, not a domain's, and users/<login>.yaml lives where the board is named.
func (*MultiBackend) SetProcess ¶
func (mb *MultiBackend) SetProcess(ctx context.Context, bd board.Board, card board.Card, process string) error
SetProcess writes in the stub's or task's domain.
func (*MultiBackend) SetProgress ¶
func (mb *MultiBackend) SetProgress(ctx context.Context, bd board.Board, card board.Card, progress int) error
SetProgress writes in the card's domain.
func (*MultiBackend) SetProject ¶
func (mb *MultiBackend) SetProject(ctx context.Context, bd board.Board, card board.Card, project string) error
SetProject may move a card into another project's domain.
func (*MultiBackend) SetRecurrence ¶
func (mb *MultiBackend) SetRecurrence(ctx context.Context, bd board.Board, card board.Card, cycle string) error
SetRecurrence writes in the card's domain.
func (*MultiBackend) SetReviewOf ¶
func (mb *MultiBackend) SetReviewOf(ctx context.Context, bd board.Board, card board.Card, reviewOf string) error
SetReviewOf may move a review card to its original's domain.
func (*MultiBackend) SetReviewRound ¶
func (mb *MultiBackend) SetReviewRound(ctx context.Context, bd board.Board, card board.Card, round int) error
SetReviewRound writes in the card's domain.
func (*MultiBackend) SetSize ¶ added in v0.37.0
func (mb *MultiBackend) SetSize(ctx context.Context, bd board.Board, card board.Card, size board.SizeKey) error
SetSize writes in the card's domain.
func (*MultiBackend) SetSprintStart ¶
func (mb *MultiBackend) SetSprintStart(ctx context.Context, bd board.Board, card board.Card, date string) error
SetSprintStart writes in the card's domain.
func (*MultiBackend) SetSprintState ¶
func (*MultiBackend) SetStage ¶
func (mb *MultiBackend) SetStage(ctx context.Context, bd board.Board, card board.Card, stage board.StageKey) error
SetStage writes in the card's domain.
func (*MultiBackend) SetStart ¶
func (mb *MultiBackend) SetStart(ctx context.Context, bd board.Board, card board.Card, date string) error
SetStart writes in the card's domain.
func (*MultiBackend) SetTask ¶
func (mb *MultiBackend) SetTask(ctx context.Context, bd board.Board, card board.Card, task string) error
SetTask may move an iteration to its task's domain.
func (*MultiBackend) SetTeam ¶
func (mb *MultiBackend) SetTeam(ctx context.Context, bd board.Board, card board.Card, team string) error
SetTeam may move a team card to another domain.
func (*MultiBackend) SetTeamPoints ¶ added in v0.37.0
func (mb *MultiBackend) SetTeamPoints(ctx context.Context, bd board.Board, team string, points int) error
SetTeamPoints writes where the TEAM is declared: its capacity is one line of the same file its sprint pointer lives in.
func (*MultiBackend) SetWeek ¶
func (mb *MultiBackend) SetWeek(ctx context.Context, bd board.Board, card board.Card, week string) error
SetWeek writes in the card's (or deadline's) domain.
func (*MultiBackend) SetZone ¶
func (mb *MultiBackend) SetZone(ctx context.Context, bd board.Board, card board.Card, zone board.ZoneKey) error
SetZone writes in the card's domain.
func (*MultiBackend) SweepGhosts ¶
func (mb *MultiBackend) SweepGhosts(_ context.Context, landed func(domain string) bool) (int, error)
SweepGhosts is the maintenance half of a move: it deletes the stale copy of every torn move whose destination has landed — landed(domain) says whether that domain's commits are pushed — one "maintenance" commit per domain, and reports how many files it removed. A duplicate that is not a move (neither copy says movedFrom) is a maintainer's to resolve and is never touched.
func (*MultiBackend) Users ¶
func (mb *MultiBackend) Users() []User
Users are the primary's links to personal repositories as of the last load — nil before any.
type Options ¶
type Options struct {
// Committer is the server's identity; it is also the author of
// unattributed actions (the sweep, an import).
Committer Identity
// AuthorEmail renders an actor's login as an email; nil gives
// <login>@aeman.
AuthorEmail func(login string) string
// Branch is the ref commits land on; empty means refs/heads/main.
Branch plumbing.ReferenceName
}
Options configures a Repo.
type PathKind ¶
type PathKind int
PathKind classifies a path in the layout.
type Process ¶
type Process struct {
ID string
Domain string
ProcessFile
Tasks []Task
}
Process is a process with its tasks.
type ProcessFile ¶
type ProcessFile struct {
Name string
Project string
Paused bool
Rank string
Created string
Extra []ExtraField
}
ProcessFile is processes/<id>/process.yaml.
func DecodeProcess ¶
func DecodeProcess(data []byte) (ProcessFile, error)
DecodeProcess parses a process file.
type Project ¶
type Project struct {
ID string
Domain string
ProjectFile
Epics []Epic
Deadlines []Deadline
}
Project is a project with its columns and deadline lines.
type ProjectFile ¶
type ProjectFile struct {
Name string
Rank string
Created string
Extra []ExtraField
}
ProjectFile is projects/<id>/project.yaml.
func DecodeProject ¶
func DecodeProject(data []byte) (ProjectFile, error)
DecodeProject parses a project file.
type RebaseResult ¶
type RebaseResult struct {
// Replayed commits were recorded again on the new tip. Dropped ones had
// nothing left to say there: a create whose path exists, an edit of a
// card the tip deleted, a change the tip already has.
Replayed, Dropped int
}
RebaseResult counts what a replay did.
type Remote ¶
type Remote struct {
URL string
Auth transport.AuthMethod
}
Remote is where a domain's repository lives: a URL and how to authenticate. Nothing forge-specific — HTTPS with a token works the same on GitHub, GitLab, Gitea and a bare repository on a server.
type Repo ¶
type Repo struct {
// contains filtered or unexported fields
}
Repo is one domain's repository: a go-git object store and the branch the board lives on. Writes are serialised — one writer per repository.
func Clone ¶
func Clone(ctx context.Context, s storage.Storer, remote Remote, opts Options, depth int) (*Repo, error)
Clone clones the remote's branch into s. depth 1 is the board's current state — the cold start; 0 is the whole history.
func (*Repo) Branch ¶
func (r *Repo) Branch() plumbing.ReferenceName
Branch is the ref the board lives on.
func (*Repo) CardLog ¶
CardLog lists the commits that touched the card, newest first. limit caps the list (0 = all); it never hides that the history is truncated. The candidates come from the path index; each is then read the precise way (entryFor), so a commit the index over-includes — a boundary, a trailer naming the card without a change — is judged, not trusted.
func (*Repo) CardLogSince ¶ added in v0.26.6
CardLogSince is CardLog cut at a boundary: the entries at or after since, and the walk stops at the first commit older than it. The day feed asks what happened on one day over every card it shows — reading each card's whole history for that is what it does not need, and on a long history that difference is seconds. A zero boundary is the whole log.
func (*Repo) ChangedPaths ¶
ChangedPaths lists the paths whose content differs between two commits' trees, sorted. A remote commit's diff goes through ParsePath from here.
func (*Repo) Commit ¶
Commit applies the writes on top of the branch tip and records them as one commit. It returns the zero hash — and makes no commit — when the writes change nothing: the same bytes again, or a delete of a file that is not there.
func (*Repo) CommitObject ¶
CommitObject reads a commit.
func (*Repo) DeepenSince ¶
DeepenSince fetches, into a shallow clone, every commit of the remote's branch newer than since, and repairs the shallow list: (old + shallows − unshallows). One round-trip, exact to the day.
func (*Repo) Fetch ¶
Fetch brings the remote's branch into the tracking ref — no depth, so a shallow clone gains no new boundaries — and reports the remote tip and whether it moved since the last fetch or push. The local branch is not touched.
func (*Repo) Maintain ¶
Maintain repacks loose objects into one pack and prunes them — go-git never packs on its own, and a commit per action leaves ~4 loose objects each. A store that cannot pack (the in-memory one) is left alone.
func (*Repo) Push ¶
Push sends the local branch to the remote. Any error may be a rejected non-fast-forward — a shallow clone reports it as "object not found" — so the caller does not classify: it fetches, and if the remote moved it re-applies and retries. On success the tracking ref is the pushed tip.
func (*Repo) PushForce ¶
PushForce writes the local branch over the remote's whatever it holds — a migration re-run with --force replaces the earlier import and anything written since. Nothing on the request path uses it: a rejected push there is re-applied on the remote's tip (Rebase), never forced.
func (*Repo) Rebase ¶
func (r *Repo) Rebase(tip plumbing.Hash) (RebaseResult, error)
Rebase moves the branch onto a freshly fetched remote tip and replays the local commits the remote has not seen, each keeping its message, trailers, author and dates. A tip that shares no history with the branch (a rewritten remote) is refused and nothing moves; a tip behind the branch changes nothing; a tip ahead of it fast-forwards.
func (*Repo) RemoteTip ¶
RemoteTip is the last known tip of the remote's branch (zero if never fetched or pushed).
func (*Repo) ResetTo ¶
ResetTo moves the local branch to a commit — onto a freshly fetched remote tip before the queue is re-applied on top of it.
func (*Repo) Unpushed ¶
Unpushed counts the local commits the remote has not seen: from the tip back to the last known remote tip (within the loaded history).
func (*Repo) UnpushedCommits ¶
UnpushedCommits lists the local commits the remote has not seen, oldest first: from the tip back to the last known remote tip (within the loaded history).
type Snapshot ¶
type Snapshot struct {
Board BoardFile
Cards []board.Card
Teams []Team
Projects []Project
Processes []Process
// Unknown lists paths that are not part of the layout; Broken lists
// paths that are, but could not be read. Neither fails the load.
Unknown []string
Broken []BrokenFile
// Aliases are roster names declared in more than one domain: the
// oldest declaration is the entry, these are the others (their cards
// still count). Ghosts are cards present in two domains mid-move: the
// copy that is NOT current. Both are for health, not for the board.
Aliases []Alias
Ghosts []Ghost
// Users are the primary's users/<login>.yaml files: each person's link
// to their personal repository. Only the primary's count.
Users []User
}
Snapshot is everything in one domain's tree at one commit.
func LoadAllAsOf ¶ added in v0.30.0
LoadAllAsOf is LoadAll at a past moment: every domain at the tree it had when that moment passed. ok is false unless EVERY domain can answer — a board missing one repository's half of that day is not that day's board, it is a smaller board that never existed.
func LoadAllAsOfDay ¶ added in v0.31.0
LoadAllAsOfDay is LoadAllAsOf for a DAY: every domain at the tree it had when the day ended, plus the cards each of them removed during it (see LoadAsOfDay). ok follows LoadAllAsOf — every domain must be able to answer, or the day is not that board's day.
func LoadAsOf ¶ added in v0.30.0
LoadAsOf reads the board as it stood at a past moment: the tree of the newest commit made at or before it. Nothing is reconstructed — the storage IS the history, so "the board on the 21st" is a tree that once was the board, not a replay of events over today's cards.
ok is false when the answer is behind the clone's horizon: a shallow boundary older than nothing we hold. Answering such a day with the oldest state we happen to have would put a stranger's values on it, so it is refused and the caller deepens (or says the history is cut). A day BEFORE the board's first commit is not that case: the board existed and was empty, and that is an answer.
func LoadAsOfDay ¶ added in v0.31.0
LoadAsOfDay is the board a DAY ended with, plus the cards the day itself removed — each in the state it was in when it went.
A day is everything that stood on it. The × takes a card off the board and the file goes, so a card worked and tidied away the same day is absent from the tree the day ended with; reading only that tree loses exactly the work the day is remembered for.
What the day removed is read from what is already written down rather than from comparing trees: every commit NAMES the cards it touched (the Aeman-Cards trailer), and the two snapshots the day is bounded by say what it began and ended with. Only the few cards that actually went are looked up in a tree. The first version diffed every commit of the day against its parent, which on a board of 2400 cards and three hundred commits a day cost 75 seconds per request — the board hung on every past day.
`from` is the previous day's last moment and `to` this one's. ok follows LoadAsOf: false when the day is behind the clone's horizon.
type SprintPointer ¶
SprintPointer is a team's current and previous sprint start.
type TeamFile ¶
type TeamFile struct {
Name string
Rank string
Created string
Sprint SprintPointer
Capacity board.Capacity
// CapacityExtra is everything in the `capacity:` mapping this server does
// not know: the old cards-a-week fields on a file written before they
// were removed, and whatever another writer put there. Extra keeps the
// unknown keys of the file's TOP level, and a known key whose value is a
// mapping needs the same promise one level down — without it, any write
// to a team file quietly ate the whole block.
CapacityExtra []ExtraField
Extra []ExtraField
}
TeamFile is teams/<id>.yaml.
type Trailers ¶
type Trailers struct {
Action string
ActionID string
Actor string
Cards []string
Changes []Change
// Extra holds the Aeman-* keys this package does not interpret
// (Aeman-Moved-From, Aeman-Migration, …); nil when there are none.
Extra map[string]string
}
Trailers is the machine-readable part of a commit message.
func ParseTrailers ¶
ParseTrailers reads the Aeman-* trailer block: the run of trailer lines at the end of the message. Anything before it is prose and is ignored.
type User ¶
type User struct {
Login string
Personal string
// Capacity is the points a week somebody set for this person; 0 = none.
Capacity int
Created string
}
User is one users/<login>.yaml: a person and their personal repository.
type UserFile ¶
type UserFile struct {
// Personal is the URL of the person's personal repository.
Personal string
// Capacity is the points a week a lead set for this person; 0 = none
// set, and the board does not invent one (board.CapacityOfPerson).
Capacity int
Created string
Extra []ExtraField
}
UserFile is users/<login>.yaml.
func DecodeUser ¶
DecodeUser parses a user file; unknown keys are kept.