All notable changes to ghx are recorded here.
Format follows Keep a Changelog.
ghb <ORG>, a read-only branches report shipped besideghx: every repo's default branch (flagged when it isn'tmain) and every other branch, classified as merged, stale, open-pr or active from the GitHub API, with org totals. New config keysstale-days,expected-defaultandinclude-archived;--format jsonfor piping. A rate limit pauses it, with a countdown under the table (or a note line while it lists repos), until the limit lifts. Seespecs/branches-report.md.- Man pages for
ghxandghb, installed by the Homebrew formula and shipped in every release archive undermanpages/.
- A GitHub rate limit while listing an org's repos now says which limit and when to retry, rather than echoing the request.
- A config value of the wrong type (
concurrency = "8"), or a float for an integer key, stops the run with an error naming the file and key, where it was silently ignored or truncated. An unknown config key gets a warning and is ignored. ghx --helpand command errors are styled with fang: a titled usage block, examples, and an error message with a pointer to--help. Piped output, and any output underNO_COLOR, has no escape codes. The exit code is still 0 on success and 1 on any error.- An existing clone that had nothing new is counted as
up to date, sopulledcounts only repos that took in new commits. - A clone fetched only for local changes names the first three changed files
(
local changes: app.py, data.csv (+2 more)). - Clone and pull get a 60-minute limit (other git commands keep 5 minutes) and give up on a connection that stays under 1000 bytes a second for 60 seconds, so a stalled transfer fails in a minute instead of holding a worker.
- A clone or pull that fails on a dropped connection, a DNS or timeout error, a rate limit or a server error is tried once more after 2 seconds. "Repository not found" and authentication failures are never retried.
- A failed repo's detail is git's last
fatal:orerror:line, not its progress output andhint:blocks. - Every existing clone is fetched on every run, so every remote branch is
backed up whatever state the working tree is in.
fetched only, with the reason, replacesskipped (local changes)for a clone whose working tree couldn't be updated. Wikis are synced the same way. - The summary marks each kind of result with a glyph beside its words:
✗failed,⇣fetched only,∅skipped (empty). A "Skipped (empty)" section lists empty repos, and a "Notes" section lists clones and pulls that carry a detail (a branch fallback, a reset after a force-push, a rename switch). - Status marks say how much of a repo came down:
✓cloned,↓pulled (new commits),–up to date,⇣fetched only (partly downloaded),⚠skipped,∅empty,✗failed. The live table's closing footer counts one class per mark (60 repos · ✓ 3 cloned · ↓ 12 pulled · – 40 up to date · ⇣ 2 fetched only · ⚠ 1 skipped · ∅ 1 empty · ✗ 1 failed · 1m12s), showing a class only when it has repos. - On a terminal, the live repo table replaces the progress bar: fixed columns that never shift, a row per repo that scrolls into the terminal's history as it finishes, the repos in flight and a progress line in the footer, and the totals at the close (wrapped between counts when the line is too long), followed by the lists of repos that need a look. Piped or redirected output keeps the static table and the full summary, with no escape codes.
- The repo table's Visibility column reads
archivedfor an archived repo (it wins overprivate), and the separate Archived column is gone. A narrow terminal now drops Last Pushed, Default Branch and Language, in that order.
ghx completion <shell>prints a shell completion script, andghx mana man page. An organization namedhelp,completionormanneeds--before it (ghx -- help <PATH>).
--deletedeletes an archived repo's wiki along with the repo, and a removed repo's wiki moves toDELETED/(or is deleted) with it, instead of being left in the output directory, never synced again.- A removed repo moved into
DELETED/no longer replaces an earlier clone of the same name there, and any work in it: the second one gets a UTC timestamp suffix (DELETED/x.20261014T013134Z), and its wiki takes the same name. - An archived repo's clone is moved into
ARCHIVED/whenever it is found outside it, not only on the run after the repo was archived: a mirror with no.ghx.jsonno longer gets a second clone inARCHIVED/, and a move that failed is tried again. An unarchived repo's clone is moved back out ofARCHIVED/(moved (unarchived)) and synced there, instead of being cloned again beside a stale copy. A wiki moves with its repo. - A clone is moved or deleted only when its origin URL names the repo;
otherwise it is left alone and reported as
skipped (origin mismatch)with the URL found. A clone is never moved onto an existing directory, which used to be deleted first: both are left in place and reported asconflict. - A repo left out by
--include,--excludeor--typeis no longer moved intoARCHIVED/, where no sync would reach it. - A repo whose HEAD names a branch that doesn't exist, which GitHub reports for
an empty repo or one whose pushes never made
main, is cloned and checked out onmain, elsemaster, else its newest branch, instead of failing. - An empty repo is skipped (
skipped (empty)) and its empty clone removed, instead of failing on every run. - A force-pushed default branch is followed when the clone has no unpushed
work, with the old tip kept under
refs/ghx/forced/<branch>/<timestamp>, instead of being skipped on every run. That includes a force-push an earlier fetch already brought in (such as the failedpull --ff-onlyof earlier versions), found through the reflog oforigin/<branch>, and the fetched-only count of local commits leaves out the ones the force-push dropped. - A default branch renamed on GitHub is switched to when nothing would be lost.
- A clone on a feature branch or a detached HEAD is fetched, and reported as fetched only with the reason, instead of failing its pull.
- A big clone is no longer killed at 5 minutes.
- A wiki that fails to clone for a reason other than "not found" is reported as
failed wikiwith git's message, not as not found.
- Hardening: ghx no longer puts the token in HTTPS clone URLs. It gives
gitthe token throughgit's environment for the length of each clone or pull (anhttp.extraheaderscoped to the clone URL's host), so the token is in no command line, no clone URL and no.git/config, and never reaches a credential helper. ghx never writes credentials to disk. - A sync cleans the origin URLs of clones made by older versions, which held the
token: in each repository it pulls, and in every clone directly under the
output directory,
DELETED/andARCHIVED/. Rotate a token that older versions used, since copies of those clones may still hold it. - The token and its header value are masked in
gitfailure messages and GitHub API errors.
ghx --version(and-v) prints the build version asghx <release>+<sha> (<build code>), for exampleghx 0.1.0+abc1234 (42), with.dirtyafter the sha for a build from a tree with uncommitted changes.make buildstamps it; a plaingo buildorgo installprintsghx dev.- Release pipeline: the Release workflow (
release.yml) bumpsversion.txtand the changelog, commits and tagsvX.Y.Z; the tag starts the Publish workflow (publish.yml), which builds six archives andchecksums.txtwith goreleaser, publishes the GitHub release, then updateslolay/homebrew-tapandlolay/scoop-bucket, in that order.make snapshotbuilds the same archives locally without publishing,make release-checkvalidates.goreleaser.yaml(also a CI job), and the Danger targetspublish-formula,publish-scoopandreleaserefuse to run without aCONFIRM_*variable. The procedure, secrets and first-release checklist are inspecs/releasing.md. - Install with
brew install lolay/tap/ghxorscoop install lolay/ghx, or download an archive from GitHub Releases, once the first release is out. version.txtholds the last released version (0.1.0until the first release), andmake bump LEVEL=patch|minor|majorrunsscripts/bump_version.pyto bump it and move these entries under the new version's heading.Makefilewith the standard targets (make helplists them;make ciis the pre-push gate), documented inMakefile.md.- Unit tests for configuration loading and precedence, repository filtering,
the
.ghx.jsonmanifest (checked byte for byte against a file written by the Python implementation) and terminal output; seespecs/testing.md. CONTRIBUTING.md,SECURITY.md,AGENTS.mdand.github/CODEOWNERS..go-versionpins the Go version used for development.
- The binary is now named
ghx(it wasghx-go), built fromcmd/ghx. Update any script or alias that callsghx-go. - The Go module path is now
github.com/lolay/ghx(it wasgithub.com/garyrudolph/ghx), sogo install github.com/lolay/ghx/cmd/ghx@latestworks. - The README is restructured, with install, authentication and configuration
(config file paths on macOS, Linux and Windows) and a CLI reference that
matches
ghx --help. It states the real Go requirement: 1.26.2 or newer.
gofmtdrift ininternal/config/resolve.go.- Repository names that can't be a directory are skipped and reported as
skipped (unusable name)instead of failing mid-sync: on every system a name that isn't a single directory name (a..in an edited.ghx.jsoncould otherwise move or delete a directory outside the output directory), and on Windows device names such asCONorNULand names ending in a dot or space. --deleted-dirand--archived-dirmust now be directories inside the output directory;.would have deleted each repo it moved.gitis looked up once, before anything is asked of GitHub, with a clear error when it's missing or older than 2.31, the minimum ghx now needs. Agitfound only in the current directory is not run.- A repository whose
git statusfails is reported as failed instead of being pulled. - On Windows, git commands run with
core.longpathson, and moves and deletes wait briefly for a file another program holds open. - On macOS and Linux, a git that ghx stops (Ctrl-C, or the 5-minute timeout) is interrupted rather than killed, so it can remove a half-made clone.
- The progress bar is drawn only on a terminal, so piped output holds no cursor escapes.