Documentation
¶
Overview ¶
Package plan turns a clustering into a reviewable, replayable description of the star lists to create and the repositories to file under them.
Index ¶
- func StalePlacements(existing []gh.List, p *Plan, ours map[string]bool, within map[string]bool) map[string][]string
- type ApplyOptions
- type ApplyResult
- type Category
- type Plan
- func (p *Plan) Categorized() int
- func (p *Plan) Markdown(w io.Writer)
- func (p *Plan) NewLists() int
- func (p *Plan) Placements() int
- func (p *Plan) Print(w io.Writer, verbose bool)
- func (p *Plan) PrintN(w io.Writer, verbose bool, limit int)
- func (p *Plan) Save(path string) error
- func (p *Plan) Stamp(t time.Time)
- func (p *Plan) Validate() error
- type Repo
- type ResetOptions
- type ResetResult
- type VerifyResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func StalePlacements ¶
func StalePlacements(existing []gh.List, p *Plan, ours map[string]bool, within map[string]bool) map[string][]string
StalePlacements reports repositories that are in a managed list the plan no longer puts them in. It is separate from Apply's additions because removals are the destructive half and are only performed when reconciling. within, when non-nil, restricts which lists may be stripped. Membership is still judged against the whole plan — a repository leaves a list only when the plan does not put it there — but the lists considered are limited to the ones the run was asked to touch.
Types ¶
type ApplyOptions ¶
type ApplyOptions struct {
// Delay paces the writes. GitHub's documented ceiling for mutating
// requests is 500 an hour, and its own advice is at least a second
// between them; going faster earns escalating secondary rate limits
// rather than a refusal.
Delay time.Duration
// Limit stops after this many repository writes; 0 means no limit. It
// counts removals as well as additions, and reconciling is refused
// alongside it — otherwise a "cautious first run" could delete every list
// the plan has dropped before the limit was ever consulted.
Limit int
// Only restricts the apply to the named categories.
Only []string
// ContinueOnError keeps going past a failed repository instead of aborting.
ContinueOnError bool
// DryRun reports what would change without changing anything.
DryRun bool
// Reconcile makes the account match the plan instead of only adding to it:
// repositories leave categories they no longer belong to, and categories
// the plan has dropped are deleted.
//
// Only lists this tool wrote are touched, recognized by the marker their
// descriptions carry. A list made by hand is left exactly alone.
Reconcile bool
// Private creates and keeps the lists private. Visibility is enforced on
// every run, not only at creation.
Private bool
// Adopt lets apply take over an existing list whose name matches a planned
// category but which this tool did not create.
//
// Taking one over replaces its description with the plan's, and that
// description carries the marker identifying a list as this tool's — so an
// adopted list becomes subject to --reconcile and to reset. It is
// announced when it happens, because neither consequence is recoverable
// from the flag's name.
Adopt bool
// Batch sends this many membership changes in one GraphQL request. One is
// the conservative default: GraphQL executes batched mutations serially so
// the effect is identical, but whether GitHub meters a batch as one
// mutating request or as many is not documented, and that is what decides
// how long a large apply takes.
Batch int
// Force makes room when the plan does not fit inside GitHub's cap by
// deleting lists this tool created that the plan no longer contains. Lists
// made by hand are never deleted, and neither is a list the plan still
// has — so this only removes what a --reconcile run would have removed
// anyway, and only as many as are needed to fit.
Force bool
Out io.Writer
}
ApplyOptions control how a plan is written back to GitHub.
type ApplyResult ¶
type ApplyResult struct {
ListsDeleted []string
ReposRemoved int
ListsCreated []string
ReposFiled int
ReposSkipped int
Errors []error
StoppedAtCap bool
// Conflicts are existing lists left untouched because their name matches a
// planned category but they were not created here.
Conflicts []string
// Adopted are lists that were taken over with --adopt: their descriptions
// were replaced and they are now managed here.
Adopted []string
// Placed maps a category to the repositories this run put in it or found
// already there: the placements the run asserts are on the account, and
// so the ones a verification can hold it to. A run that stopped at
// --limit, or skipped a category over a name conflict, asserts nothing
// about the rest. A dry run records only what was already in place.
Placed map[string][]string
}
ApplyResult reports what actually happened.
func Apply ¶
func Apply(ctx context.Context, c *gh.ListsClient, p *Plan, opt ApplyOptions) (*ApplyResult, error)
Apply creates the planned lists and files each repository into its category.
The whole account is read once, so every decision below is made against known state rather than by asking GitHub about each repository in turn. A repository is written at most once: its complete set of memberships is computed first — additions from the plan, removals from reconciling — and submitted in a single mutation, because updateUserListsForItem replaces the whole set. Membership the plan knows nothing about is carried across untouched, which is what stops an apply emptying a hand-curated list.
type Category ¶
type Category struct {
Name string `json:"name"`
Description string `json:"description"`
Terms []string `json:"terms"`
// Exists is true when a list of this name is already on the account, in
// which case applying only adds repositories to it.
Exists bool `json:"exists"`
Cohesion float64 `json:"cohesion"`
Repos []Repo `json:"repos"`
}
Category is one proposed or existing star list.
type Plan ¶
type Plan struct {
Version int `json:"version"`
GeneratedAt time.Time `json:"generated_at"`
User string `json:"user"`
Backend string `json:"backend"`
// Settings records the tuning the plan was produced with, so a plan is
// self-describing: the categories depend on a dozen weights and
// thresholds, and without them a plan cannot be reproduced or explained
// months later except by remembering the command line.
//
// Keys are the flag names of the run that built the categories. An
// incremental run does not rebuild them, so it carries these forward
// unchanged from the plan it extended — whatever clustering flags were on
// its own command line — and records itself separately:
// "incremental" is "true", "incremental-from" is the GeneratedAt of the
// plan extended, in RFC 3339, and the "place-" keys
// (place-min-similarity, place-outlier-sigmas, place-multi-list,
// place-multi-list-ratio) are the placement knobs the newcomers were
// filed with. Only the latest pass is recorded: each one overwrites them.
Settings map[string]string `json:"settings,omitempty"`
TotalStars int `json:"total_stars"`
Categories []Category `json:"categories"`
Unassigned []Repo `json:"unassigned"`
}
Plan is the full proposal. It is written to disk so that the categorization a human reviewed is exactly the one that later gets applied.
func Build ¶
func Build(user string, repos []gh.Repo, sp *embed.Space, res *cluster.Result, labels []cluster.Label, existing []gh.List) *Plan
Build assembles the plan from the clustering result. Repositories the clusterer refused to place are reported rather than hidden: an honest "these 40 stars fit no category" is more useful than 40 bad placements.
func (*Plan) Categorized ¶
Categorized counts the distinct repositories the plan places in at least one list.
func (*Plan) Markdown ¶
Markdown renders the plan as a document, useful as a starred-repos index even if the lists are never applied.
func (*Plan) NewLists ¶
NewLists counts the categories that do not already exist on the account, which is what applying the plan would have to create.
func (*Plan) Placements ¶
Placements counts repository-to-list pairs, which is what applying the plan actually writes. It exceeds Categorized when repositories belong to more than one list.
func (*Plan) PrintN ¶
PrintN writes the plan, showing at most limit categories; 0 shows all. The plan file is unaffected — this only bounds what reaches the terminal, since a thirty-category plan scrolls its own summary off the screen.
func (*Plan) Stamp ¶
Stamp records when the plan was produced. It is separate from Build so that clustering stays a pure function of its inputs.
func (*Plan) Validate ¶ added in v0.2.0
Validate checks a plan before anything is written from it.
The plan file exists to be reviewed and edited by hand, and an edit can break what Build guarantees: a name GitHub will refuse, a description that lost the marker this tool recognises its own lists by, two categories that differ only in case and so would silently become one list. Every problem is reported in a single error, one per line, so a plan is fixed in one pass rather than one refusal at a time.
A category with no repositories is not an error: Build never writes one, and Apply and Verify skip one deliberately, so emptying a category is a legitimate way of dropping it without deleting the entry.
type Repo ¶
type Repo struct {
FullName string `json:"full_name"`
Description string `json:"description,omitempty"`
Similarity float64 `json:"similarity"`
Stars int `json:"stars,omitempty"`
PushedAt time.Time `json:"pushed_at,omitzero"`
StarredAt time.Time `json:"starred_at,omitzero"`
}
Repo is a repository placed in a category.
Stars and the two dates are carried through because the plan is also read by people and by scripts: they are what you sort an index by, and what tells a dormant project from a live one. They cost nothing to record — every one of them was already fetched with the star list.
type ResetOptions ¶
ResetOptions control how Reset tears down the lists this tool created.
type ResetResult ¶
type ResetResult struct {
Deleted []string
// Kept are lists left alone because they were not created here.
Kept []string
Errors []error
}
ResetResult reports what Reset did, or would do.
func Reset ¶
func Reset(ctx context.Context, c *gh.ListsClient, user string, opt ResetOptions) (*ResetResult, error)
Reset deletes every star list this tool created, identified by the marker its descriptions carry, and leaves every other list untouched. It is the inverse of apply: a way back to a clean account after experimenting, without deleting anything the user made by hand.
type VerifyResult ¶
type VerifyResult struct {
// Missing maps a category to the repositories the plan places there that
// the account does not have.
Missing map[string][]string
// MissingLists are planned categories with no list at all.
MissingLists []string
Checked int
}
VerifyResult reports the difference between what the plan asserts and what the account actually holds.
func Verify ¶
func Verify(ctx context.Context, c *gh.ListsClient, p *Plan, only []string, out io.Writer) (*VerifyResult, error)
Verify re-reads the account and checks that every placement the plan asserts is really there.
This exists because a mutation reporting success is not proof the placement is visible. updateUserListsForItem replaces a repository's whole set of lists, so a partial write, or one racing another client, would not surface as an error. Reading the result back is the only real confirmation.
It costs one page read per list, not one per repository, so it is cheap enough to run after every apply.
func VerifyApplied ¶ added in v0.2.0
func VerifyApplied(ctx context.Context, c *gh.ListsClient, p *Plan, only []string, res *ApplyResult, out io.Writer) (*VerifyResult, error)
VerifyApplied checks what an apply run left the account asserting, which is not always the whole plan.
A run that completed asserts every placement in the categories it was asked for, less any it skipped because a list of that name was not created here. A run that stopped at --limit asserts only the placements it made or found in place: holding it to the rest would fail by construction, since it never wrote them — which is what made `apply --limit N --verify` always exit 1. A nil res verifies the plan as it stands.
func (*VerifyResult) OK ¶
func (v *VerifyResult) OK() bool
OK reports whether the account matches the plan.