gitstore

package
v0.38.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

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

View Source
const BoardPath = "board.yaml"

BoardPath is the board's own file, primary domain only.

View Source
const SchemaVersion = 1

SchemaVersion is the layout version this server writes and understands.

Variables

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

View Source
var ErrBadID = errors.New("gitstore: id too short to shard")

ErrBadID is returned for an id that cannot be placed in the layout.

View Source
var ErrBadIDFormat = errors.New("gitstore: not a ulid")

ErrBadIDFormat is returned for a string that is not a ULID.

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

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

View Source
var ErrNoDomains = errors.New("gitstore: a board needs at least its primary domain")

ErrNoDomains is returned when a board has nothing to read.

View Source
var ErrNoRemoteTip = errors.New("gitstore: remote has no such branch")

ErrNoRemoteTip is returned when the remote has no branch to sync with.

View Source
var ErrNotFound = errors.New("gitstore: not found")

ErrNotFound is returned by readers for a missing path.

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

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

View Source
var ErrUnknownDomain = fmt.Errorf("gitstore: unknown domain")

ErrUnknownDomain names a domain that is not part of the board.

Functions

func CardPath

func CardPath(id string) (string, error)

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

func DeadlinePath(projectID, id string) string

DeadlinePath is one deadline line of a project.

func DeriveID

func DeriveID(t time.Time, namespace string, keys ...string) string

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

func EncodeBoard(f BoardFile) ([]byte, error)

EncodeBoard renders board.yaml. The schema is always written, even at zero, because its absence means "older than versioning".

func EncodeCard

func EncodeCard(f CardFile) ([]byte, error)

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 EncodeEpic

func EncodeEpic(f EpicFile) ([]byte, error)

EncodeEpic renders an epic 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 EncodeTeam

func EncodeTeam(f TeamFile) ([]byte, error)

EncodeTeam renders a team file.

func EncodeUser

func EncodeUser(f UserFile) ([]byte, error)

EncodeUser renders a user file.

func EpicPath

func EpicPath(projectID, id string) string

EpicPath is one column of a project.

func IDTime

func IDTime(id string) (time.Time, error)

IDTime reads the moment an id was minted for.

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

func IterationID(task, week string) string

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

func MigrateSchema(r *Repo) (bool, error)

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 NewID

func NewID(t time.Time) string

NewID mints an id for the given moment with random tail bits.

func ProcessPath

func ProcessPath(id string) string

ProcessPath is a process's own file.

func ProjectPath

func ProjectPath(id string) string

ProjectPath is a project's own file.

func ScopeCards

func ScopeCards(ctx context.Context) []string

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 TaskPath

func TaskPath(processID, id string) string

TaskPath is one task of a process — a card file, minus placement.

func TeamPath

func TeamPath(id string) string

TeamPath is where a team lives; the no-team group is the team "_".

func UserPath

func UserPath(login string) string

UserPath is users/<login>.yaml — a person's link to their personal repository, read from the primary only.

func WithScope

func WithScope(ctx context.Context, a Action) (context.Context, func() (plumbing.Hash, error))

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

func (b *Backend) AddNote(ctx context.Context, _ board.Board, card board.Card, text string) error

AddNote appends a note with its own id, the actor and the time.

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

func (b *Backend) DeleteCard(ctx context.Context, _ board.Board, card board.Card) error

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

func (b *Backend) LoadBoard(_ context.Context, boardID string) (board.Board, error)

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

func (b *Backend) LoadCards(ctx context.Context, _ board.Board, ids []string) ([]board.Card, error)

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

func (b *Backend) SetAccumulate(ctx context.Context, _ board.Board, card board.Card, on bool) error

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

func (b *Backend) SetDay(ctx context.Context, _ board.Board, card board.Card, day string) error

SetDay sets or clears the end day.

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

func (b *Backend) SetDoneAt(ctx context.Context, _ board.Board, card board.Card, day string) error

SetDoneAt writes the board day the card counts as finished on, without touching the progress that normally sets it.

func (*Backend) SetEpic

func (b *Backend) SetEpic(ctx context.Context, _ board.Board, card board.Card, epic string) error

SetEpic files the card under a column, or renames an epic stub.

func (*Backend) SetLeftAt

func (b *Backend) SetLeftAt(ctx context.Context, _ board.Board, card board.Card, day string) error

SetLeftAt sets or clears the day a personal card was left behind on.

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

func (b *Backend) SetPaused(ctx context.Context, _ board.Board, card board.Card, paused bool) error

SetPaused pauses or resumes a process.

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 (b *Backend) SetSprintState(ctx context.Context, _ board.Board, team, current, previous string) error

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

func (b *Backend) SetStart(ctx context.Context, _ board.Board, card board.Card, date string) error

SetStart sets or clears the start day.

func (*Backend) SetTask

func (b *Backend) SetTask(ctx context.Context, _ board.Board, card board.Card, task string) error

SetTask links an iteration to its task.

func (*Backend) SetTeam

func (b *Backend) SetTeam(ctx context.Context, _ board.Board, card board.Card, team string) error

SetTeam sets or clears the team.

func (*Backend) SetTeamPoints added in v0.37.0

func (b *Backend) SetTeamPoints(ctx context.Context, _ board.Board, team string, points int) error

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.

func (*Backend) SetWeek

func (b *Backend) SetWeek(ctx context.Context, _ board.Board, card board.Card, week string) error

SetWeek sets the week — of a card, or of a deadline stub.

func (*Backend) SetZone

func (b *Backend) SetZone(ctx context.Context, _ board.Board, card board.Card, zone board.ZoneKey) error

SetZone sets or clears the zone.

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

func DecodeBoard(data []byte) (BoardFile, error)

DecodeBoard parses board.yaml. A schema newer than SchemaVersion is refused; an older or missing one comes back as is, for migration.

type BrokenFile

type BrokenFile struct {
	Path string
	Err  error
}

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.

func DecodeCard

func DecodeCard(id string, data []byte) (CardFile, error)

DecodeCard parses a card file. id is the card's id — it lives in the path, not the file — and is stamped onto the card.

type Change

type Change struct {
	Card string
	Kind string
	From string
	To   string
}

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 Domain

type Domain struct {
	Name string
	Repo *Repo
}

Domain is one repository of a board.

type Epic

type Epic struct {
	ID     string
	Domain string
	EpicFile
}

Epic is one column.

type EpicFile

type EpicFile struct {
	Name    string
	Rank    string
	Created string
	Extra   []ExtraField
}

EpicFile is projects/<pid>/epics/<id>.yaml.

func DecodeEpic

func DecodeEpic(data []byte) (EpicFile, error)

DecodeEpic parses an epic file.

type ExtraField

type ExtraField struct {
	Key   string
	Value *yaml.Node
}

ExtraField is an unknown front-matter key, kept in first-seen order.

type FileWrite

type FileWrite struct {
	Path string
	Data []byte
}

FileWrite is one file's new content; nil Data deletes the file.

type Ghost

type Ghost struct {
	ID      string
	Domain  string
	Current string
}

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 Identity

type Identity struct {
	Name  string
	Email string
}

Identity is a name/email pair for author or committer.

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

func (mb *MultiBackend) DeleteCard(ctx context.Context, bd board.Board, card board.Card) error

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

func (mb *MultiBackend) LoadBoard(_ context.Context, boardID string) (board.Board, error)

LoadBoard merges every domain into the board the service expects.

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

func (mb *MultiBackend) SetLeftAt(ctx context.Context, bd board.Board, card board.Card, day string) error

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 (mb *MultiBackend) SetSprintState(ctx context.Context, bd board.Board, team, current, previous string) error

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.

const (
	PathUnknown PathKind = iota
	PathBoard
	PathCard
	PathTeam
	PathProject
	PathEpic
	PathDeadline
	PathProcess
	PathTask
	PathUser
)

The kinds of path the layout has.

func ParsePath

func ParsePath(p string) (PathKind, []string)

ParsePath is the layout's inverse: what a path is and the ids in it. A remote commit's diff goes through it so the cache reloads exactly the objects that changed.

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 Init

func Init(s storage.Storer, opts Options) (*Repo, error)

Init creates the repository in an empty storer.

func Open

func Open(s storage.Storer, opts Options) *Repo

Open wraps an existing repository.

func (*Repo) Branch

func (r *Repo) Branch() plumbing.ReferenceName

Branch is the ref the board lives on.

func (*Repo) CardLog

func (r *Repo) CardLog(id string, limit int) (Log, error)

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

func (r *Repo) CardLogSince(id string, since time.Time) (Log, error)

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

func (r *Repo) ChangedPaths(from, to plumbing.Hash) ([]string, error)

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

func (r *Repo) Commit(a Action, writes []FileWrite) (plumbing.Hash, error)

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

func (r *Repo) CommitObject(h plumbing.Hash) (*object.Commit, error)

CommitObject reads a commit.

func (*Repo) DeepenSince

func (r *Repo) DeepenSince(ctx context.Context, remote Remote, since time.Time) error

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

func (r *Repo) Fetch(ctx context.Context, remote Remote) (plumbing.Hash, bool, error)

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

func (r *Repo) Head() plumbing.Hash

Head is the branch tip, or the zero hash when the branch is unborn.

func (*Repo) Maintain

func (r *Repo) Maintain() error

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

func (r *Repo) Push(ctx context.Context, remote Remote) error

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

func (r *Repo) PushForce(ctx context.Context, remote Remote) error

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

func (r *Repo) ReadFile(p string) ([]byte, error)

ReadFile returns a file's bytes at the branch tip.

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

func (r *Repo) RemoteTip() plumbing.Hash

RemoteTip is the last known tip of the remote's branch (zero if never fetched or pushed).

func (*Repo) ResetTo

func (r *Repo) ResetTo(h plumbing.Hash) error

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

func (r *Repo) Storer() storage.Storer

Storer exposes the underlying object store (for sync and tests).

func (*Repo) Unpushed

func (r *Repo) Unpushed() (int, error)

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

func (r *Repo) UnpushedCommits() ([]*object.Commit, error)

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

func (*Repo) Walk

func (r *Repo) Walk(from plumbing.Hash, fn func(*object.Commit) (bool, error)) error

Walk visits commits from `from` back along first parents. fn returns false to stop. A shallow boundary is visited and not crossed; a root ends the walk.

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 Load

func Load(r *Repo) (Snapshot, error)

func LoadAll

func LoadAll(domains []Domain) (Snapshot, error)

LoadAll reads every domain at its branch tip and merges the snapshots.

func LoadAllAsOf added in v0.30.0

func LoadAllAsOf(domains []Domain, at time.Time) (Snapshot, bool, error)

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

func LoadAllAsOfDay(domains []Domain, from, to time.Time) (Snapshot, bool, error)

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

func LoadAsOf(r *Repo, at time.Time) (Snapshot, bool, error)

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

func LoadAsOfDay(r *Repo, from, to time.Time) (Snapshot, bool, error)

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.

func LoadAt

func LoadAt(r *Repo, h plumbing.Hash) (Snapshot, error)

LoadAt reads the snapshot at a commit — the tip, or a past one for the day-state replay.

type SprintPointer

type SprintPointer struct {
	Current  string
	Previous string
}

SprintPointer is a team's current and previous sprint start.

type Task

type Task struct {
	ID     string
	Domain string
	CardFile
}

Task is one process task — a card file.

type Team

type Team struct {
	ID     string
	Domain string
	TeamFile
}

Team is teams/<id>.yaml with its id.

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.

func DecodeTeam

func DecodeTeam(data []byte) (TeamFile, error)

DecodeTeam parses a team file.

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

func ParseTrailers(msg string) Trailers

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

func DecodeUser(data []byte) (UserFile, error)

DecodeUser parses a user file; unknown keys are kept.

Jump to

Keyboard shortcuts

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