mirror

package
v1.8.9 Latest Latest
Warning

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

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

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

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

ErrNotFound is returned when a name resolves to nothing on the remote.

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

func RealGit(ctx context.Context, dir string, args ...string) (string, error)

RealGit runs the git binary.

func Workspace

func Workspace(ctx context.Context, git Git, root string, e Entry) (string, error)

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 NewForge

func NewForge(base, token string) *Client

NewForge returns a client for a Gitea-compatible forge.

func NewGitHub

func NewGitHub(token string) *Client

NewGitHub returns a client for github.com.

func (*Client) Resolve

func (c *Client) Resolve(ctx context.Context, org, name string) (*Repo, error)

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.

func (*Client) Tip

func (c *Client) Tip(ctx context.Context, org, name, branch string) (string, error)

Tip returns the commit a branch points at, accepting either field name — Gitea answers `commit.id` and GitHub `commit.sha`, and a reconcile that only read one would silently see an empty tip on the other and treat "unknown" as "equal".

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.

const (
	// Native: the forge is canonical and github.com is a push-mirror. Our own code.
	Native Direction = "native"
	// Mirror: an upstream we do not own. Read-only here, so it keeps following.
	Mirror Direction = "mirror"
)

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.

func (Entry) String

func (e Entry) String() string

type Git

type Git func(ctx context.Context, dir string, args ...string) (string, error)

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.

const (
	InSync   Outcome = "in-sync"
	Moved    Outcome = "moved"
	Diverged Outcome = "diverged"
	Skipped  Outcome = "skipped"
	Failed   Outcome = "failed"
)

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

func Plan(ctx context.Context, gh *Client, entries []Entry) ([]Planned, []error)

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

func (r Repo) Direction() 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

type Result struct {
	Entry   Entry
	Outcome Outcome
	From    string
	To      string
	Detail  string
}

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.

func Summary

func Summary(rs []Result) (counts map[Outcome]int, needsHuman []Result)

Summary counts outcomes, and reports whether a human is needed.

Jump to

Keyboard shortcuts

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