93a3129444064c4d19b4cb49e24b6de4bd0e06a6
markd
  Fri Oct 2 21:41:15 2026 -0700
Restore the superTrack nesting fix. refs #38460

Taking this off master was over-cautious. It is wanted on its own: #38460 is a
real bug, a superTrack given a parent passes tdbQuery -check -strict and is then
silently dropped at load, and the fix should get into a release rather than wait
on a trackDb change that uses it.

Nothing on master nests yet, and the fix is behaviour-identical where nothing
does: trackDb built with it and with the released hgTrackDb is byte-identical on
hg38, hs1 and mm39, and hgTracks renders the same rows and supertrack controls
as dev.

The TSS trackDb change that uses this stays reverted, on superTrackNesting, until
this ships. Whoever releases it: trackDbToTxt has to be rebuilt too, or the
curated hub it writes names a parent stanza it did not emit.

diff --git src/hg/lib/trackDbCustom.c src/hg/lib/trackDbCustom.c
index 4fddeb2ca84..a0e6a842070 100644
--- src/hg/lib/trackDbCustom.c
+++ src/hg/lib/trackDbCustom.c
@@ -613,30 +613,34 @@
 }
 
 char *maybeSkipHubPrefix(char *track)
 {
 if (!startsWith("hub_", track))
     return track;
 
 char *nextUnderBar = strchr(track + sizeof "hub_", '_');
 
 if (nextUnderBar)
     return nextUnderBar + 1;
 
 return track;
 }
 
+#define maxSuperTrackDepth 10
+// No real trackDb nests supertracks anywhere near this deep; the limit is only
+// here so a malformed parent chain cannot spin forever.
+
 void trackDbSuperMarkup(struct trackDb *tdbList)
 /* Set trackDb from superTrack setting */
 {
 struct trackDb *tdb;
 struct hash *superHash = hashNew(0);
 char *setting = NULL;
 char *words[3];
 int wordCt = 0;
 
 /* find supertracks, setup their settings */
 for (tdb = tdbList; tdb != NULL; tdb = tdb->next)
     {
     setting = trackDbLocalSetting(tdb, "superTrack");
     if (!setting)
         continue;
@@ -645,55 +649,82 @@
         {
         if (!hashLookup(superHash, tdb->track))
             {
             hashAdd(superHash, maybeSkipHubPrefix(tdb->track), tdb);
             tdbMarkAsSuperTrack(tdb);
             if ((wordCt > 1) && sameString("show", words[1]))
                 tdb->isShow = TRUE;
             }
         }
     freeMem(words[0]);
     }
 /* adjust settings on supertrack members after verifying they have
  * a supertrack configured in this trackDb */
 for (tdb = tdbList; tdb != NULL; tdb = tdb->next)
     {
-    if (tdbIsSuperTrack(tdb) || tdb->parent != NULL)
+    if (tdb->parent != NULL)
         continue;
     setting = trackDbLocalSetting(tdb, "parent");
-    if (!setting)
+    // A superTrack may name a parent superTrack, which nests it one level deeper.
+    // Its own "superTrack on" is not a parent reference, so only a non-super may
+    // fall back to the old style, where the setting holds the parent's name.
+    if (!setting && !tdbIsSuperTrack(tdb))
         setting = trackDbLocalSetting(tdb, "superTrack");  // Old style
     if (!setting)
         continue;
     wordCt = chopLine(cloneString(setting), words);
     assert(differentString("on", words[0])); // already weeded out "superTrack on"
     char *parentName = maybeSkipHubPrefix(words[0]);
     tdb->parent = hashFindVal(superHash, parentName);
     if (tdb->parent == tdb)
         {
         warn("Track %s lists itself as its own parent, ignoring.", tdb->track);
         tdb->parent = NULL;
         }
     if (tdb->parent)
         {
         tdbMarkAsSuperTrackChild(tdb);
         tdb->parentName = cloneString(parentName);
         if (wordCt > 1)
             tdb->visibility = max(0, hTvFromStringNoAbort(words[1]));
         }
     freeMem(words[0]);
     }
+
+/* Now that every link is set, make sure the supertracks form a tree.  Nesting
+ * means the parent chain is walked rather than looked at one level, so a loop
+ * would hang every walker instead of being harmless.  Only a supertrack can
+ * close a loop here, since nothing else is given a parent by this routine, so
+ * breaking it at a supertrack names the track whose setting is wrong and leaves
+ * the members attached to their folder. */
+for (tdb = tdbList; tdb != NULL; tdb = tdb->next)
+    {
+    if (!tdbIsSuperTrack(tdb) || tdb->parent == NULL)
+        continue;
+    struct trackDb *ancestor;
+    int depth = 0;
+    for (ancestor = tdb->parent; ancestor != NULL; ancestor = ancestor->parent)
+        {
+        if (ancestor == tdb || ++depth > maxSuperTrackDepth)
+            {
+            warn("Supertrack %s is its own ancestor through its parent setting, "
+                 "ignoring that setting.", tdb->track);
+            tdb->parent = NULL;
+            break;
+            }
+        }
+    }
 hashFree(&superHash);
 }
 
 char *trackDbOrigAssembly(struct trackDb *tdb)
 /* return setting from trackDb, if any */
 {
 return (trackDbSetting(tdb, "origAssembly"));
 }
 
 void trackDbPrintOrigAssembly(struct trackDb *tdb, char *database)
 /* Print lift information from trackDb, if any */
 {
 char *origAssembly = trackDbOrigAssembly(tdb);
 if (origAssembly)
     {