Documentation
¶
Overview ¶
Package mirror reconciles which repositories live on git.hanzo.ai and which direction each one syncs.
It replaces a Python script (hanzoai/mirrors reconcile.py + sync.py) that drove the forge over untyped HTTP. That script worked, and its two worst bugs were both consequences of being untyped against a typed API:
- The direction table was matched against a GitHub org LISTING rather than resolved per name. A listing reports a repo only under its CURRENT name in its CURRENT org, so six entries whose repos had been renamed or transferred were never visited even once — and nothing failed, because a name absent from a listing raises nothing. Here Resolve is a distinct method from List, so the two cannot be confused at a call site.
- `mirror` meant two different things — a vestigial git config flag and the forge's actual push refspecs — and reading the wrong one produced a confident wrong conclusion about whether a push could delete history. Direction is one typed value here, with one meaning.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("mirror: not found")
ErrNotFound is returned when a name resolves to nothing on the remote.
var Owned = map[string]bool{ "hanzo-apps": true, "hanzo-dart": true, "hanzo-docs": true, "hanzo-go": true, "hanzo-js": true, "hanzo-ml": true, "hanzo-rs": true, "hanzoai": true, "hanzo-inc": true, "hanzobot": true, "hanzodao": true, "hanzoid": true, "hanzokv": true, "hanzozt": true, "luxfi": true, }
Owned is the set of orgs this may touch. An ALLOWLIST, not a denylist, and the difference is the whole point.
The script this replaces carried a regex of forbidden names, which meant the forbidden names were written down in our source — the exact thing the rule against them forbids. It needed that regex because it swept entire orgs, so anything could turn up in a listing.
This never enumerates an org: it acts only on the explicitly declared table. The one residual risk is a declared repo being TRANSFERRED into an org we do not own, which Plan checks after resolution. Naming what we own answers that without naming anything we do not, and fails closed on an org nobody thought about — including ones that do not exist yet. Owned lists the orgs whose repos this program may touch. An ALLOWLIST, not a denylist: it refuses by default, so an org nobody has thought about yet is refused rather than swept, and a lookalike that merely contains one of our names is refused too.
It is deliberately a SUPERSET of the orgs in repos.json. A declared repo can be renamed or transferred, and `owned` guards the name GitHub resolves it to — so an org with no entry today is still the org a repo may land in tomorrow. Three declared repos had already moved into hanzo-apps that way.
Functions ¶
func Workspace ¶
Workspace prepares the object store for one repo and returns its path.
One store per repo, under a root that MAY survive between runs: git's fetch negotiation offers whatever is already here, so a kept root makes every later run a delta. It is only ever an optimisation — an empty root is equally correct, just slower — so this needs no volume to be right, and losing the volume costs time rather than correctness.
Types ¶
type Branch ¶
type Branch struct {
Name string `json:"name"`
Commit struct {
ID string `json:"id"`
SHA string `json:"sha"`
} `json:"commit"`
}
Branch is the tip of one branch.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client talks to one forge or to github.com. Two instances, one type: the APIs are compatible for everything here, and giving them separate clients would mean two places to fix a bug in either.
func (*Client) Resolve ¶
Resolve answers "what is org/name TODAY", following renames and transfers.
This is deliberately NOT List-and-search. GET /repos/{org}/{name} follows a rename and a transfer permanently; a listing does neither, which is how six repos went unreconciled and silent. Keeping these as separate methods means a caller has to choose, rather than reach for whichever is at hand.
type Direction ¶
type Direction string
Direction is which way a repository syncs. There are exactly two, and the difference is which side is allowed to be authoritative.
type Entry ¶
type Entry struct {
Org string `json:"org" yaml:"org"`
Name string `json:"name" yaml:"name"`
Direction Direction `json:"direction" yaml:"direction"`
}
Entry is one declared repository: where it lives here, and which way it syncs.
This is data, not code. Adding a repo used to be a source edit to a list literal inside the script; it is now a line of config, which is what "declared state" has to mean if the word is doing any work.
type Git ¶
Git runs git in a working directory. Injectable so the fast-forward decision is testable without a network or a real repository — the FF guarantee is the safety property here, and one that can only be tested against live remotes is one nobody tests.
The directory is a parameter rather than the process's own, because every command here needs a repository to hold objects in and a scheduled job starts in none. Run from a bare working directory git answers "fatal: not a git repository", and this package would have reported that as GitHub being unreadable.
type Outcome ¶
type Outcome string
Outcome is what a reconcile did to one repo. Diverged is not an error state to be retried — it is a question for a person, and saying so is the whole point.
type Planned ¶
type Planned struct {
Entry
// GitHub is the repo the entry resolves to NOW. Its full name differs from the
// entry whenever GitHub has renamed or moved the repo, which is the case the
// listing-based predecessor could not see at all.
GitHub *Repo
// Moved is true when the entry's name is no longer the remote's name.
Moved bool
}
Planned is one entry resolved against the remote: what we declared, and what the remote says that name is today.
func Plan ¶
Plan resolves every declared entry against GitHub, in the order it will be acted on: smallest first, so a budget spent on one enormous repo cannot starve every small one behind it.
Two entries resolving to ONE GitHub repo is a config bug, not a merge: both would push-mirror to the same remote and overwrite each other. The first wins and the collision is reported, because silently dropping one is how a repo stops syncing without anyone being told.
type Repo ¶
type Repo struct {
FullName string `json:"full_name"`
Owner struct {
Login string `json:"login"`
} `json:"owner"`
Name string `json:"name"`
Private bool `json:"private"`
Empty bool `json:"empty"`
Mirror bool `json:"mirror"`
CloneURL string `json:"clone_url"`
Desc string `json:"description"`
Size int `json:"size"`
Default string `json:"default_branch"`
}
Repo is a repository as the remote reports it. The fields are the ones a reconcile decision actually reads; the wire carries far more, and decoding only what is used keeps a schema change from silently altering behaviour.
func (Repo) Direction ¶
Direction reports the direction this repo is currently configured for.
Derived from the forge's own `mirror` field and nothing else. The git config flag of the same name is vestigial — the push path passes explicit refspecs that override it — so reading the config would answer a different question than the one being asked.
type Result ¶
Result is one repo's reconcile.
func FastForward ¶
func FastForward(ctx context.Context, git Git, work string, p Planned, forgeURL, ghURL, branch, forgeTip, ghTip string) Result
FastForward moves the forge's branch up to GitHub's, or refuses and says why.
The guarantee is GIT's, not this function's: the refspec carries no leading '+', so a non-fast-forward is refused by git itself. That ordering matters — a check written here could be reasoned around by a later edit, whereas a missing '+' cannot be. A push mirror once pruned the v1.0-v1.31 tag range off an IAM repo and the objects were then collected; every safety property here exists because of something like that.
Divergence returns Diverged with both tips and touches nothing. Two people disagreeing about history is not a race for a sync job to settle: settling it destroys whichever side lost.
func Reconcile ¶
func Reconcile(ctx context.Context, forge, gh *Client, git Git, forgeBase, work string, plan []Planned) []Result
Reconcile brings every declared NATIVE repo's forge branch up to GitHub's.
Only native entries: a mirror tracks an upstream we do not own, and pushing INTO one would fight the thing it exists to follow.
One repo's failure never ends the run. A reconcile that stops at the first problem leaves every repo after it unexamined, and the ordering is by size, so the unexamined ones would be arbitrary.