80ab15c036bf35894b59df383c03c51cbeff30be
chmalee
  Thu May 14 10:29:53 2026 -0700
Stop myVariants from clobbering regular custom tracks by giving it its own cart variable (mvCtfile_<db>) instead of overloading ctfile_<db>, refs #37553

diff --git src/hg/lib/customTrack.c src/hg/lib/customTrack.c
index 0aac5925f5c..198a98ade7f 100644
--- src/hg/lib/customTrack.c
+++ src/hg/lib/customTrack.c
@@ -541,30 +541,33 @@
 FILE *f = mustOpen(fileName, "w");
 
 #ifdef DEBUG
 struct dyString *ds = dyStringNew(100);
 if (!fileExists(fileName))
     {
     dyStringPrintf(ds, "chmod 666 %s", fileName);
     system(ds->string);
     }
 #endif
 
 struct customTrack *track;
 struct dyString *ds = dyStringNew(0);
 for (track = trackList; track != NULL; track = track->next)
     {
+    /* myVariants tracks live in mvCtfile_<db>; skip so they aren't duplicated here. */
+    if (track->tdb && track->tdb->type && startsWithWord("myVariants", track->tdb->type))
+        continue;
     /* may be coming in here from the table browser.  It has wiggle
      *	ascii data waiting to be encoded into .wib and .wig
      */
     if (track->wigAscii)
         {
         /* HACK ALERT - calling private method function in customFactory.c */
         track->maxChromName = hGetMinIndexLength(genomeDb); /* for the loaders */
 #ifdef PROGRESS_METER
 	track->progressFile = 0;
 #endif
         wigLoaderEncoding(track, track->wigAscii, ctDbUseAll());
         ctAddToSettings(track, "tdbType", track->tdb->type);
         ctAddToSettings(track, "wibFile", track->wibFile);
         }
 
@@ -588,43 +591,51 @@
     if (!track->dbTrack)
         {
         struct bed *bed;
         for (bed = track->bedList; bed != NULL; bed = bed->next)
             bedOutputN(bed, track->fieldCount, f, '\t', '\n');
         }
     }
 dyStringFree(&ds);
 carefulClose(&f);
 }
 
 void customTracksSaveCart(char *genomeDb, struct cart *cart, struct customTrack *ctList)
 /* Save custom tracks to trash file for database in cart */
 {
 char *ctFileVar = customTrackFileVar(cartString(cart, "db"));
-if (ctList)
+boolean hadRegular = isNotEmpty(cartOptionalString(cart, ctFileVar));
+/* Count only tracks that customTracksSaveFile will actually persist. */
+int persistCount = 0;
+struct customTrack *ct;
+for (ct = ctList; ct != NULL; ct = ct->next)
+    if (!(ct->tdb && ct->tdb->type && startsWithWord("myVariants", ct->tdb->type)))
+        persistCount++;
+if (persistCount > 0)
     {
     static struct tempName tn;
     trashDirFile(&tn, "ct", CT_PREFIX, ".bed");
     char *ctFileName = tn.forCgi;
     cartSetString(cart, ctFileVar, ctFileName);
     if (printSaveList)
-        fprintf(stderr, "customTrack: saved %d in %s\n", slCount(ctList), ctFileName);
+        fprintf(stderr, "customTrack: saved %d in %s\n", persistCount, ctFileName);
     customTracksSaveFile(genomeDb, ctList, ctFileName);
     }
-else
+else if (hadRegular)
     {
-    /* no custom tracks remaining for this assembly */
+    /* user just emptied this assembly's regular ctfile; drop the pointer
+     * and the per-track ct_* cart vars tied to it. */
     cartRemove(cart, ctFileVar);
     cartRemovePrefix(cart, CT_PREFIX);
     }
 }
 
 boolean customTrackIsCompressed(char *fileName)
 /* test for file suffix indicating compression */
 {
 char *fileNameDecoded = cloneString(fileName);
 cgiDecode(fileName, fileNameDecoded, strlen(fileName));
 boolean result = 
     (endsWith(fileNameDecoded,".gz") || 
      endsWith(fileNameDecoded,".Z")  ||
      endsWith(fileNameDecoded,".zip")  ||
      endsWith(fileNameDecoded,".bz2"));
@@ -938,82 +949,98 @@
                 errCatch->message->string[len - 1] = 0;
             }
         warn("Custom track loading error (%s): failed to load custom tracks. "
              "This is a temporary internal error, please refresh your browser. If you continue to experience this issue"
              "please reach out to genome-www@soe.ucsc.edu and send us a session link "
              "where this error occurs",
              errCatch->message->string);
         loadFailed = TRUE;
         }
     errCatchFree(&errCatch);
     // If there was a failure in loading the custom tracks, return immediately -- don't try to
     // add or merge in new custom tracks.  The cartRemove statements below will be skipped, so we
     // can try again next click.
     if (loadFailed)
         return NULL;
+    }
+
+/* Layer in the user's myVariants tracks from mvCtfile_<db>. */
+char mvVar[256];
+safef(mvVar, sizeof mvVar, MYVARIANTS_FILE_VAR_PREFIX "%s", genomeDb);
+char *mvFile = cartOptionalString(cart, mvVar);
+if (isNotEmpty(mvFile) && fileExists(mvFile))
+    {
+    struct customTrack *mvList = NULL;
+    struct errCatch *mvCatch = errCatchNew();
+    if (errCatchStart(mvCatch))
+        mvList = customFactoryParse(genomeDb, mvFile, TRUE, fileName, NULL);
+    errCatchEnd(mvCatch);
+    if (mvCatch->gotError)
+        {
+        if (isNotEmpty(mvCatch->message->string))
+            warn("myVariants load error: %s", mvCatch->message->string);
+        cartRemove(cart, mvVar);
+        mvList = NULL;
+        }
+    errCatchFree(&mvCatch);
+    ctList = slCat(ctList, mvList);
+    }
 
-    /* handle selected tracks -- update doc, remove, etc. */
+/* Handle a selected track from hgTrackUi (remove or doc update). */
 char *selectedTable = NULL;
 if (cartVarExists(cart, CT_DO_REMOVE_VAR))
     selectedTable = cartOptionalString(cart, CT_SELECTED_TABLE_VAR);
 else
     selectedTable = cartOptionalString(cart, CT_UPDATED_TABLE_VAR);
 if (selectedTable)
     {
     for (ct = ctList; ct != NULL; ct = nextCt)
         {
         nextCt = ct->next;
         if (sameString(selectedTable, ct->tdb->track))
             {
             if (cartVarExists(cart, CT_DO_REMOVE_VAR))
                 {
-                    /* remove a track if requested, e.g. by hgTrackUi */
                 removedCt = TRUE;
-                    /* myVariants tracks need type-specific cleanup so the
-                     * SQL-backed entry doesn't reappear next page load.
-                     * When the helper handled the cart cleanup itself we
-                     * skip the wide cartRemovePrefix below so other-
-                     * assembly per-db labels survive. */
+                /* myVariants has SQL-backed state to clean up; helper returns
+                 * TRUE when it handled the per-track cart cleanup itself. */
                 boolean handledByMyVariants =
                     myVariantsHandleCtRemoval(ct, cart, genomeDb);
                 slRemoveEl(&ctList, ct);
                 if (!handledByMyVariants)
                     {
-                        /* remove visibility variable */
                     cartRemove(cart, selectedTable);
-                        /* remove configuration variables */
                     char buf[128];
                     safef(buf, sizeof buf, "%s.", selectedTable);
                     cartRemovePrefix(cart, buf);
                     }
                 cartRemove(cart, CT_DO_REMOVE_VAR);
                 }
             else
                 {
                 if (html && differentString(html, ct->tdb->html))
                     {
                     ct->tdb->html = html;
                     changedCt = TRUE;
                     }
                 }
             break;
             }
         }
     }
 cartRemove(cart, CT_DO_REMOVE_VAR);
 cartRemove(cart, CT_SELECTED_TABLE_VAR);
-    }
 
 /* merge new and old tracks */
 numAdded = slCount(newCts);
 /* add delay even if numAdded==0 because that can be when the loading
  * of the custom tracks failed.  The try is worth the penalty.
  */
 if (numAdded > 0)
     {
     static int botCheckMult = 0;
     if (0 == botCheckMult)      // only on first time through here
 	{                       // default is 1 when not specified
 	char *val = cfgOptionDefault("customTracks.botCheckMult", "1");
         botCheckMult = sqlSigned(val);
         if (botCheckMult < 1)   // protect against negative value
 	    botCheckMult = 1;   // default is 1, no maximum check here