97557b2dab79bd19b0345f1ea49a233c191300ae
braney
Sat Sep 12 06:59:07 2026 -0700
hgConfCatalog: cite skipMalformedCgiPairs where the tree actually reads it, refs #37925
The row cited lib/cheapcgi.c, which is where the flag takes effect but not
where it is read: the kent libraries cannot read hg.conf at all. The read is
hg/lib/hgConfig.c:227, which hands the setting to cheapcgi through
cgiSkipMalformedPairs. The nightly reconcile had been reporting the row as
citing a file the read has left.
diff --git src/hg/utils/hgConfCatalog/hgConfCatalog.py src/hg/utils/hgConfCatalog/hgConfCatalog.py
index 4d2713464ec..700bb694013 100755
--- src/hg/utils/hgConfCatalog/hgConfCatalog.py
+++ src/hg/utils/hgConfCatalog/hgConfCatalog.py
@@ -1,3181 +1,3181 @@
#!/usr/bin/env python3
"""hgConfCatalog.py - the registry of hg.conf variables.
Third of the browser configuration inventories, after cartTrackVarCatalog.py
(#37838, track-scoped cart variables) and urlCommandCatalog.py (#37923, CGI URL
parameters). This one covers hg.conf: the per-machine configuration file every
CGI reads at startup through the cfgOption* accessors in hg/lib/hgConfig.c.
What makes hg.conf different from the other two, and the reason this file
needed a mechanism the others do not have:
A cart variable belongs to a user and a URL parameter belongs to a request.
An hg.conf variable belongs to a machine, and a machine we do not control.
Mirrors, the GBiB and the GBiC all carry their own hg.conf, so a variable is
reachable long after the tree stops caring about it, and deleting one is a
compatibility decision rather than a cleanup.
On top of that, the tree deliberately manufactures short-lived hg.conf
variables as part of the release process. A user-visible feature is expected
to ship dark behind cfgOptionBooleanDefault(name, FALSE) so it can sit on
master through QA without blocking the release train, then have its default
flipped to TRUE once it is released, then have the flag deleted. That last
step is the one nobody does. Nothing in the tree records that a flag was
meant to be temporary, so shipped gates accumulate: showTutorial has been
defaulting TRUE since v466, greyBarIcons since v492, and both are still
branch points in hgTracks today.
So the registry separates two populations that look identical in the source:
gate introduced to gate a release. Temporary by intent. Has a lifecycle
(added, flipped, sunset) and is expected to be deleted.
knob a genuine deployment switch a mirror is entitled to set forever.
isGbib, browser.dumpStack and hgta.disableAllTables are knobs. These
are exempt from sunsetting and saying so explicitly is what keeps the
report from crying wolf.
The distinction cannot be made mechanically, which is why it is curated here
rather than harvested. Everything else about a gate's lifecycle is mechanical:
harvestHgConf.py --age dates each variable's first read, and each boolean
flag's first TRUE default, out of the git history, mapped onto the CGI_VERSION
in effect at the time. So `added` and `flipped` come from the tree and only
`sunset` is a judgement call. A gate with no explicit sunset gets
flipped + KEEP_AFTER_FLIP, which is the release after mirrors have had a cycle
to object.
Sunset policy, all in versions (releases are about three weeks apart):
KEEP_AFTER_FLIP = 4 once the default is TRUE the feature is public. Keep
the flag four more releases so a mirror or hgwbeta can
switch it back off without a code change, then delete
it along with every branch that reads it.
QA_GRACE = 6 a gate still defaulting FALSE this long after it landed
is not being gated, it is being forgotten. Either turn
it on or delete the feature.
Verification status. Rows carry verified=True only where the classification
was confirmed by reading the code at the cited source file. --check counts what
is left, because a variable described as a permanent knob when it is really a
forgotten gate defeats the purpose of the exercise.
Usage:
hgConfCatalog.py --json out.json
hgConfCatalog.py --html out.html
hgConfCatalog.py --check # counts and internal consistency
hgConfCatalog.py --reconcile # diff the catalog against the tree
hgConfCatalog.py --reconcile --verbose # ... with the standing drift
hgConfCatalog.py --sunset # what should be deleted, and when
hgConfCatalog.py --sunset-new # ... only what changed since the backlog
hgConfCatalog.py --redundant # conf lines a flipped default made pointless
hgConfCatalog.py --redundant --verbose # ... and the files it read
hgConfCatalog.py --update-baseline # accept the current backlog
hgConfCatalog.py --refresh --sunset-new # ... rebuilding the age cache
hgConfCatalog.py --cache /tmp/ages.json --refresh --sunset-new
# ... without writing into the tree
--reconcile is the mode meant for a nightly cron: it prints nothing and exits 0
when the tree holds no setting the catalog has not classified, and exits 1 with
the list when somebody has added one. --sunset-new is the same shape for the
release side, and is what belongs in the weekly build's wrap-up: it reports the
gates that went overdue or stalled since the backlog file was last accepted, and
the ones that were cleaned up, and says nothing about the standing backlog.
--sunset itself reports every overdue gate on every run whether or not anything
changed. That is the right thing for a person working the list down and the
wrong thing for anything automated, which is why it is a separate mode.
--redundant is the third leg of the same idea and the one that closes the loop.
Flipping a default to TRUE does not turn the feature on anywhere: it was already
on, on every machine somebody had appended the flag to by hand. What the flip
does is make those lines pointless, and a pointless line is not harmless, since
while it is there the flag reads on locally whatever the tree says. So this
mode reads the hg.conf files it can see from hgwdev, joins them against the
catalog, and names the lines to delete. Like --reconcile it is silent and exits
0 when there is nothing to do, which is what lets the nightly cron run it and
mail only the news.
"""
import argparse
import html
import json
import os
import re
import sys
# Sunset policy, in releases. See the module docstring.
KEEP_AFTER_FLIP = 4
QA_GRACE = 6
# The gates already known to be overdue or stalled, so --sunset-new can report
# what changed rather than the whole standing list. Regenerate with
# --update-baseline and commit it; the diff is then the release-to-release
# history of the backlog, in the git log rather than in somebody's memory.
BACKLOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"hgConfGateBacklog.txt")
# Floor on how many settings a working scan finds; well under the real count.
# See the check in reconcile().
MIN_TREE_NAMES = 150
REDMINE = "https://redmine.gi.ucsc.edu/issues/%d"
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
def h(name, kind, src, default=None, note=None, public=False, verified=False,
role=None, sunset=None, env=None, deprecated=False, family=None,
required=False, ticket=None, debatable=None):
"""One catalog entry.
name the hg.conf setting name
kind what sort of setting: path, table, profile, credential, url,
email, limit, flag, branding, debug, internal, dead
src source file the tree reads it in, with no line number. A
line is derived data: it is recomputed for display from the
harvester, so an edit above a read is not a citation to fix
default compiled-in default if the read supplies one
role for boolean flags only: "gate" (a release gate, temporary by
intent, subject to sunsetting) or "knob" (a permanent
deployment switch, exempt). Every cfgOptionBooleanDefault
flag in the tree must be one or the other; --reconcile
enforces that
sunset release by which a gate should be gone from the tree. Omit to
take the default of flipped + KEEP_AFTER_FLIP; the age cache
supplies the flip version
public documented for mirror operators in product/ex.hg.conf, or
belongs there
verified True if the classification was confirmed by reading src
env environment variable that overrides it, via cfgOptionEnv
family groups members of one prefix family, for the docs
required read with cfgVal, so the CGI dies if it is absent
deprecated the feature it configures is gone or going
ticket Redmine ticket that introduced or tracks it
debatable why this row's gate-or-knob call could reasonably go the other
way. Set it rather than picking a side quietly: the whole
point of separating the two is that someone has decided, and a
decision nobody argued with is not the same as a decision.
--check lists these as the review agenda
"""
d = {"name": name, "kind": kind, "src": src, "public": public,
"verified": verified}
for key, val in (("default", default), ("note", note), ("role", role),
("sunset", sunset), ("env", env), ("family", family),
("ticket", ticket), ("debatable", debatable)):
if val is not None:
d[key] = val
if required:
d["required"] = True
if deprecated:
d["deprecated"] = True
return d
# ---------------------------------------------------------------------------
# how hg.conf is read at all
# ---------------------------------------------------------------------------
ACCESSORS = {
"cfgOption": "Returns the value or NULL. Absent means off.",
"cfgOptionDefault": "Returns the value or a compiled-in default.",
"cfgOptionBooleanDefault":
"Boolean with a compiled-in default. Accepts yes/no, on/off, "
"true/false. This is the accessor used to gate a release.",
"cfgVal": "Returns the value or errAborts. The CGI will not run without it.",
"cfgOptionEnv":
"Environment variable first, then hg.conf. Note the argument order: "
"the environment name comes first and is not an hg.conf setting.",
"cfgOptionEnvDefault": "As cfgOptionEnv, with a compiled-in default.",
"cfgOption2":
"Reads prefix.suffix. The prefix is usually a runtime value, which is "
"how one call site serves db.host, central.host and every other "
"profile at once.",
"cfgOptionDefault2": "As cfgOption2, with a compiled-in default.",
}
BOUNDARY = (
"hg.conf is read once per CGI invocation and is never written by the "
"browser. It is not user state: nothing in a session, a saved session or "
"a URL can change it, which is exactly why it is the right place to gate "
"a feature during a release. A mirror's copy is outside our control, so "
"removing a variable has to be treated as an interface change, not a "
"cleanup. Precedence for a value is: the environment (only for the "
"cfgOptionEnv settings, and only where the CGI allows it), then hg.conf, "
"then the compiled-in default."
)
PROFILE_SUFFIXES = {
"what":
"A database profile is a set of hg.conf settings sharing one prefix, "
"read through cfgOption2(profileName, suffix) where profileName is a "
"runtime value. So these suffixes are legal under any profile prefix, "
"and none of the resulting names appears as a literal anywhere in the "
"tree. This is why product/ex.hg.conf documents "
"archivecentral.password while a search of the source finds nothing. "
"Any profile also has a failover profile, named with the \"slow-\" "
"prefix in front of the main profile's name (sqlProfileGetFailover at "
"jksql.c:409), which is where slow-db comes from. excludeDbs is the "
"one suffix that belongs only to a failover profile, and the one that "
"is not read through cfgOption2: jksql.c:1325 builds the whole name "
"with safef and reads it with cfgOption, which is why no harvest of "
"the tree finds it even though ex.hg.conf:36 documents "
"slow-db.excludeDbs and goldenPath/help/gbib.html explains it twice. "
"Its value is a comma-separated list of databases that exist only on "
"the local server, so the failover connection is never opened for "
"them.",
"src": "hg/lib/jksql.c",
"suffixes": ["host", "port", "socket", "user", "password", "db",
"verifyServerCert", "ca", "caPath", "cert", "key", "cipher",
"crl", "crlPath", "excludeDbs"],
"knownProfiles": ["db", "slow-db", "central", "cart", "customTracks",
"archivecentral", "backupcentral", "myStuff", "myGenome",
"rrcentral", "pq", "rtdb", "cdw"],
}
# ---------------------------------------------------------------------------
# release gates: boolean flags that exist to hold a feature back
# ---------------------------------------------------------------------------
# These are the reason this catalog has a sunset mode. Each was added so a
# user-visible change could sit on master without shipping. Ordered by age so
# the backlog is visible at a glance.
RELEASE_GATES = {
"what": "Boolean flags introduced to ship a feature dark during a release. "
"Temporary by intent: each should be deleted once the feature it "
"guards is public and mirrors have had a cycle to object.",
"vars": [
h("showTxCodonNumbers", "flag", "hg/hgTracks/cds.c", default="FALSE",
role="gate", verified=True, ticket="38298",
note="A second codon number in the gene-track codon mouseover, counted in the "
"transcript's own coordinates, for a transcript that aligns with bases the "
"assembly does not have. The number the browser has always shown is counted "
"along the genome, so for those transcripts it disagrees with the number NCBI "
"reports, by one codon per three missing bases. It affects 58 coding "
"transcripts on hg38's primary chromosomes but 656 on canFam3, and gets worse "
"the poorer the assembly. With the gate on, such codons also draw in the "
"browser's existing query-insertion orange with a \"!\" after the codon "
"number. Read once in txCodonNumbersEnabled(); with it off no alignment is "
"looked up, no codon carries a transcript number, and the rendering and the "
"mouseover are byte-identical to before. Off during QA; flip to TRUE once "
"released."),
- h("skipMalformedCgiPairs", "flag", "lib/cheapcgi.c", default="FALSE",
+ h("skipMalformedCgiPairs", "flag", "hg/lib/hgConfig.c", default="FALSE",
role="gate", verified=True, ticket="38340",
note="Step over a CGI or cookie pair that has no =value, instead of losing the "
"pair after it or aborting the request. Covers parseCookies in cheapcgi, "
"hgSession's session backup, and refreshNamedSessionCustomTracks. The query "
"string parsers were fixed unconditionally under #38335 and do not read this "
"flag. The kent libraries cannot read hg.conf, so hgConfig.c hands the "
"setting to cheapcgi through cgiSkipMalformedPairs. Off during QA; flip to "
"TRUE once released."),
h("collectionHubCopyOnWrite", "flag", "hg/lib/cart.c", default="FALSE",
role="gate", verified=True, ticket="38273",
note="Copy a track collection's generated hub file when the program that writes "
"it (only hgCollection) asks for a copy, instead of on every session load. "
"With it off, loading any session that carries a collection copies the hub to "
"a new trash file and registers that copy in hgcentral.hubStatus, which is "
"where 81% of the RR's 3.1M hubStatus rows came from. Read in "
"cartCollectionHubCopyOnWrite() and branched on at four places in three "
"files: cart.c makes the copy when cartRequestLocalHubCopy() was called, and "
"skips a hub the cart already owns in copyLocalHubs(); "
"cartCopyLocalHubsOnSessionLoad() does nothing (that one is the old behavior, "
"and it plus its five callers in cart.c and hgSession.c go away with the "
"gate); sessionData.c's saveTrackFile splits a hub that came from another "
"session. Off during QA; flip to TRUE once released."),
h("bigNet", "flag", "hg/lib/trackHub.c", default="FALSE",
role="gate", verified=True, ticket="20824",
note="The bigNet track type, a net of pairwise alignments in a "
"bigBed, and quickLift support for it. Read once in "
"trackHubBigNetEnabled(); with it off a hub that declares "
"\"type bigNet\" fails to load with an unsupported-type error, "
"quickLift refuses the type, and neither hubCheck nor the hubApi "
"lists it among the supported types."),
h("showMouseovers", "flag", "hg/hgTracks/config.c", default="FALSE",
role="gate", verified=True,
note="Mouseover text on track items instead of the browser's own "
"title tooltips. Added v446 and still defaulting FALSE, which "
"makes it the oldest gate in the tree that never shipped. Four "
"call sites across config.c and imageV2.c. Either the feature "
"is wanted, in which case flip it, or it is not, in which case "
"the flag and the code behind it should go."),
h("storeUserFiles", "flag", "hg/hgHubConnect/hgHubConnect.c",
default="FALSE", role="gate", verified=True,
note="Hub space, the user file store behind hgHubConnect's upload "
"wizard. Added v447 and briefly defaulted TRUE around v454 "
"before going back to FALSE, so the history shows a flip that "
"was reverted. Four call sites."),
h("hgSession.shortLink", "flag", "hg/hgSession/hgSession.c",
default="FALSE", role="gate", verified=True,
note="Short session links. Added v374 and never flipped, which is "
"the longest-running dark feature here."),
h("showHubApiKey", "flag", "hg/hgHubConnect/hgHubConnect.c",
default="FALSE", role="gate", verified=True,
note="Expose the hub API key UI. Shares its call site with "
"storeUserFiles, so the two should be retired together."),
h("autoBlatBigPsl", "flag", "hg/hgBlat/hgBlat.c",
default="FALSE", role="gate", verified=True, ticket="32751",
note="Always create a custom track from BLAT results, so a result "
"page can be reopened and shared. The read at hgBlat.c:2972 "
"overrides the file-scope autoBigPsl, which is initialised "
"FALSE at hgBlat.c:60 under the comment \"DEFAULT VALUE change "
"to TRUE in future\", and eleven branches downstream test it. "
"Filed as a knob until that was read, on the strength of the "
"default being an identifier the harvester could not resolve; "
"a flag whose own source says to flip it later is a gate."),
h("blatShowLocus", "flag", "hg/hgBlat/hgBlat.c", default="FALSE",
role="gate", verified=True,
note="Show the genomic locus alongside BLAT results."),
h("modernAlignPage", "flag", "hg/hgc/hgc.c", default="FALSE",
role="gate", verified=True, ticket="37893",
note="Use the modern single-page base-by-base alignment view (the one "
"hgBlat's new results page uses) for ordinary alignment clicks too "
"- mRNA/EST, PSL and similar hgc details - instead of the classic "
"two-frame