d121edc0bd2809f1d6de6185a497e6a288958479
braney
  Tue May 12 09:18:29 2026 -0700
hgConvert quickLift: skip pre-lifted tracks, append-and-merge hub file, per-track remove UI, refs #37535

In hgConvert / trackHubBuild:
- Skip tracks that already came from a quickLift hub (quickLiftUrl / quickLifted setting) so they don't get re-lifted to a new destination.
- Append new track stanzas to an existing per-source hub file instead of overwriting it; new stanzas get priorities after the existing max; duplicate track names are skipped. Also avoids re-emitting a parent supertrack that's already in the file.

New public quickLiftHubRemoveTrack(cart, sourceDb, trackName) in trackHub.c rewrites the per-source hub file with the named stanza removed plus all descendant stanzas (parent reference cascade, transitive).

hgTrackUi: adds a "Remove from QuickLift" link next to "Duplicate track" for any tdb carrying a quickLiftDb setting. The link hits hgTrackUi_op=quickLiftRemove which calls quickLiftHubRemoveTrack, hides the track in the cart, and 302s to hgTracks. The op argument cart var is qlSourceDb (renamed from quickLiftSourceDb to avoid colliding with the quickLift.* prefix used elsewhere; values cloned out of the cart hash before cartRemove so the helper doesn't see freed strings).

hgTracks: adds a small "x" icon (printQuickLiftDelIcon) on tracks in a quickLift group, suppressed on the synthetic bigQuickLiftChain track. JS onQuickLiftDelIconClick fires the same hgTrackUi_op endpoint via synchronous XHR and removes every TD whose icon matches the deleted data-track, so the row goes away in both the QuickLift group and the Visible Tracks group.

hubConnect cart handling fixes shaken out by the above:
- hubConnectRemakeTrackHubVar's cart-var prefix is now "quickLift." (with the trailing dot) instead of "quickLift", so unrelated keys like qlSourceDb no longer get parsed as hubId/db and crash cart loading on every CGI. Also skips entries whose hubStatus lookup returned NULL.
- hubConnectStatusListFromCart no longer calls removeQuickListReference when the current db isn't the lift's destination. A side trip to another assembly between two lifts to the same destination was deleting the earlier attachment's cart var; just skip attaching this load and leave the cart alone so the lift survives the round trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git src/hg/lib/hubConnect.c src/hg/lib/hubConnect.c
index 9afd0e3038d..a5874268b06 100644
--- src/hg/lib/hubConnect.c
+++ src/hg/lib/hubConnect.c
@@ -119,39 +119,43 @@
     struct slPair *hubVar;
     boolean firstOne = TRUE;
     for (hubVar = hubVarList; hubVar != NULL; hubVar = hubVar->next)
         {
 	if (cartBoolean(cart, hubVar->name))
 	    {
 	    if (firstOne)
 		firstOne = FALSE;
 	    else
 		dyStringAppendC(trackHubs, ' ');
 	    dyStringAppend(trackHubs, hubVar->name + prefixLength);
 	    }
 	}
     slPairFreeList(&hubVarList);
 
-    // now see if we should quicklift any hubs
+    // now see if we should quicklift any hubs.  Use the "quickLift." prefix
+    // (with trailing dot) so unrelated vars like "quickLifted*" or
+    // "quickLiftSourceDb" don't get matched and parsed as <hubId>.<db>.
     struct sqlConnection *conn = hConnectCentral();
     char query[2048];
-    hubVarList = cartVarsWithPrefix(cart, "quickLift");
+    hubVarList = cartVarsWithPrefix(cart, "quickLift.");
     for (hubVar = hubVarList; hubVar != NULL; hubVar = hubVar->next)
         {
         unsigned hubNumber = atoi(hubVar->name + strlen("quickLift."));
         sqlSafef(query, sizeof(query), "select hubUrl from hubStatus where id='%d'", hubNumber);
         char *hubUrl = sqlQuickString(conn, query);
+        if (hubUrl == NULL)
+            continue;
         char *errorMessage;
         unsigned hubId = hubFindOrAddUrlInStatusTable(cart, hubUrl, &errorMessage);
 
         if (firstOne)
             firstOne = FALSE;
         else
             dyStringAppendC(trackHubs, ' ');
         dyStringPrintf(trackHubs, "%d:%s", hubId,(char *)hubVar->val);
         }
     hDisconnectCentral(&conn);
 
     cartSetString(cart, hubConnectTrackHubsVarName, trackHubs->string);
     dyStringFree(&trackHubs);
     cartRemove(cart, hgHubConnectRemakeTrackHub);
     }
@@ -342,35 +346,35 @@
         sqlSafef(query, sizeof(query), "select fromDb, toDb, path from %s where id = \"%s\"", quickLiftChainTable(), colon);
         struct sqlResult *sr = sqlGetResult(conn, query);
         char **row;
         char *replaceDb = NULL;
         char *quickLiftChain = NULL;
         char *toDb = NULL;
         while ((row = sqlNextRow(sr)) != NULL)
             {
             replaceDb = cloneString(row[0]);
             toDb = cloneString(row[1]);
             quickLiftChain = cloneString(row[2]);
             break; // there's only one
             }
         sqlFreeResult(&sr);
 
-        // don't load quickLift hubs that aren't for us
+        // Only attach the quickLift hub when we're on its destination db.
+        // Side trips to other dbs leave the cart var alone so the lift is
+        // still there when the user comes back.
         if ((db == NULL) || sameOk(toDb, hubConnectSkipHubPrefix(db)))
             hub = hubConnectStatusForIdExt(conn, id, replaceDb, toDb, quickLiftChain);
-        else
-            removeQuickListReference(cart, id, toDb);
         }
     if (hub != NULL)
 	{
 	if (!isEmpty(hub->errorMessage) && (strstr(hub->hubUrl, "hgComposite") != NULL))
             {
             // custom collection hub has disappeared.   Remove it from cart
             cartSetString(cart, hgHubConnectRemakeTrackHub, "on");
             char buffer[1024];
             safef(buffer, sizeof buffer, "hgHubConnect.hub.%d", id);
             cartRemove(cart, buffer);
             }
         else
             slAddHead(&hubList, hub);
 	}
     }