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/trackHub.c src/hg/lib/trackHub.c index 5dfa88c8d31..593c8a950b3 100644 --- src/hg/lib/trackHub.c +++ src/hg/lib/trackHub.c @@ -45,30 +45,31 @@ #include "hubConnect.h" #include "trix.h" #include "vcf.h" #include "vcfUi.h" #include "htmshell.h" #include "bigBedFind.h" #include "customComposite.h" #include "interactUi.h" #include "bedTabix.h" #include "hic.h" #include "hui.h" #include "chromAlias.h" #include "trashDir.h" #include "hgConfig.h" #include "cartTrackDb.h" +#include "quickLift.h" #ifdef USE_HAL #include "halBlockViz.h" #endif struct grp *trackHubGrps = NULL; // global with grps loaded from track hubs static struct hash *hubCladeHash; // mapping of clade name to hub pointer static struct hash *hubAssemblyHash; // mapping of assembly name to genome struct static struct hash *hubAssemblyUndecoratedHash; // mapping of undecorated assembly name to genome struct static struct hash *hubOrgHash; // mapping from organism name to hub pointer static struct trackHub *globalAssemblyHubList; // list of trackHubs in the user's cart static struct hash *trackHubHash; static boolean isValidSeqNameChar(char c) /* Return TRUE if c is a valid character for a sequence name: [A-Za-z0-9._-]. */ @@ -1548,30 +1549,153 @@ // we don't reuse userdata paths since they are in save sessions if ((hubName != NULL) && strstr(hubName, "userdata")) hubName = NULL; if ((hubName == NULL) || ((fd = open(hubName, 0)) < 0)) { trashDirDateFile(&hubTn, "quickLift", "hub", ".txt"); hubName = cloneString(hubTn.forCgi); cartSetString(cart, buffer, hubName); } return hubName; } +struct quickLiftStanza +/* One track stanza parsed out of a quickLift hub file. */ + { + struct quickLiftStanza *next; + char *name; /* bare track name */ + char *parent; /* bare parent track name, or NULL */ + struct dyString *text; /* full stanza text including final newline */ + }; + +static char *firstWordClone(char *s) +/* Return a clone of the first whitespace-delimited word of s, or NULL. */ +{ +s = skipLeadingSpaces(s); +if (isEmpty(s)) + return NULL; +char *sp = skipToSpaces(s); +int len = (sp != NULL) ? (sp - s) : (int)strlen(s); +return cloneStringZ(s, len); +} + +boolean quickLiftHubRemoveTrack(struct cart *cart, char *sourceDb, char *trackName) +/* Remove a track stanza from the quickLift hub file for sourceDb, along with + * any descendant stanzas (transitively) whose parent is being removed. + * Returns TRUE if at least one stanza was removed. */ +{ +char buffer[4096]; +safef(buffer, sizeof buffer, "%s-%s", quickLiftCartName, sourceDb); +char *filename = cartOptionalString(cart, buffer); +if (filename == NULL) + return FALSE; + +struct lineFile *lf = lineFileMayOpen(filename, TRUE); +if (lf == NULL) + return FALSE; + +char *bareName = trackHubSkipHubName(trackName); +struct dyString *header = dyStringNew(0); +struct quickLiftStanza *stanzaList = NULL; +struct quickLiftStanza *cur = NULL; +char *line; +int lineSize; + +/* Pass 1: read the file into a header + list of stanzas, recording each + * stanza's name and (if any) parent. */ +while (lineFileNext(lf, &line, &lineSize)) + { + char *trim = skipLeadingSpaces(line); + if (startsWithWord("track", trim)) + { + AllocVar(cur); + cur->text = dyStringNew(0); + cur->name = firstWordClone(trim + 5); + slAddHead(&stanzaList, cur); + } + else if (cur != NULL && startsWithWord("parent", trim)) + { + if (cur->parent == NULL) + cur->parent = firstWordClone(trim + 6); + } + + struct dyString *target = (cur != NULL) ? cur->text : header; + dyStringAppend(target, line); + dyStringAppendC(target, '\n'); + } +slReverse(&stanzaList); +lineFileClose(&lf); + +/* Build a removal set: start with the named track, then iterate adding any + * stanza whose parent is already in the set, until the set is stable. */ +struct hash *removeSet = hashNew(0); +hashStore(removeSet, bareName); +boolean changed = TRUE; +while (changed) + { + changed = FALSE; + struct quickLiftStanza *s; + for (s = stanzaList; s != NULL; s = s->next) + { + if (s->name == NULL || s->parent == NULL) + continue; + if (hashLookup(removeSet, s->name) != NULL) + continue; + if (hashLookup(removeSet, s->parent) != NULL) + { + hashStore(removeSet, s->name); + changed = TRUE; + } + } + } + +boolean removedAny = FALSE; +struct dyString *out = dyStringNew(0); +struct quickLiftStanza *s; +for (s = stanzaList; s != NULL; s = s->next) + { + if (s->name != NULL && hashLookup(removeSet, s->name) != NULL) + removedAny = TRUE; + else + dyStringAppend(out, s->text->string); + } + +if (removedAny) + { + FILE *f = mustOpen(filename, "w"); + chmod(filename, 0666); + fputs(header->string, f); + fputs(out->string, f); + fclose(f); + } + +dyStringFree(&header); +dyStringFree(&out); +hashFree(&removeSet); +for (s = stanzaList; s != NULL; s = s->next) + { + dyStringFree(&s->text); + freeMem(s->name); + freeMem(s->parent); + } +slFreeList(&stanzaList); +return removedAny; +} + static char *vettedTracks[] = /* tracks that have been tested with quickLift */ { "decipherContainer", "decipherSnvs", "omimLocation", "omimAvSnp", "ncbiRefSeq", "clinvar", "clinvarSubLolly", "pubs", "pubsBlat", "pubsMarkerBand", "pubsMarkerSnp", "pubsMarkerGene", @@ -1843,123 +1967,189 @@ safef(buffer, sizeof buffer, "%d", priority); hashReplace(tdb->settingsHash, "priority", cloneString(buffer)); struct dyString *dy = trackDbString(cart, tdb); fprintf(f, "%s\n", dy->string); } static boolean checkCartVisibility(struct cart *cart, struct trackDb *tdb) { char *cartVis = cartOptionalString(cart, tdb->track); if (cartVis != NULL) tdb->visibility = hTvFromString(cartVis); return (tdb->visibility != tvHide); } -static void walkTree(FILE *f, char *db, struct cart *cart, struct trackDb *tdb, struct dyString *visDy, struct trackDb **badList) -/* walk tree looking for visible tracks to output to hub. */ +static boolean isFromQuickLiftHub(struct trackDb *tdb) +/* True if this tdb came from a quickLift hub (already a lifted shadow track). + * Such tracks must not be lifted again. */ { -unsigned priority = 1; +return trackDbSetting(tdb, "quickLiftUrl") != NULL || + trackDbSetting(tdb, "quickLifted") != NULL; +} + +static void walkTree(FILE *f, char *db, struct cart *cart, struct trackDb *tdb, struct dyString *visDy, struct trackDb **badList, struct hash *existingTracks, unsigned startPriority) +/* walk tree looking for visible tracks to output to hub. Skip tracks that already + * came from a quickLift hub, and skip tracks whose name is already present in + * the existing hub file. */ +{ +unsigned priority = startPriority; struct hash *haveSuper = newHash(0); struct trackDb *tdbNext = NULL; for(; tdb; tdb = tdbNext) { tdbNext = tdb->next; + if (isFromQuickLiftHub(tdb)) + continue; + + if (existingTracks != NULL && + hashLookup(existingTracks, trackHubSkipHubName(tdb->track)) != NULL) + continue; + boolean isVisible = FALSE; if (tdb->parent == NULL) isVisible = checkCartVisibility(cart, tdb); else if (isParentVisible(cart, tdb) && isSubtrackVisible(cart, tdb)) // child of supertrack { if (hashLookup(haveSuper, tdb->parent->track) == NULL) // output yet? { //if (checkCartVisibility(cart, tdb->parent)) + { + char *bareParent = trackHubSkipHubName(tdb->parent->track); + if (existingTracks == NULL || + hashLookup(existingTracks, bareParent) == NULL) { tdb->parent->visibility = hTvFromString("tvShow"); outTrack(f, cart, tdb->parent, priority++); - + } hashStore(haveSuper, tdb->parent->track); } } isVisible = checkCartVisibility(cart, tdb); } if (isVisible && validateTdb(cart, db, tdb, badList)) { hashRemove(tdb->settingsHash, "superTrack"); // this gets inherited by subTracks(?) // is this a custom track? char *tdbType = trackDbSetting(tdb, "tdbType"); if (tdbType != NULL) { hashReplace(tdb->settingsHash, "type", tdbType); hashReplace(tdb->settingsHash, "shortLabel", trackDbSetting(tdb, "name")); hashReplace(tdb->settingsHash, "longLabel", trackDbSetting(tdb, "description")); } outTrack(f, cart, tdb, priority++); } } } +static void readExistingHubTracks(char *filename, struct hash *trackNames, unsigned *retMaxPriority) +/* Scan an existing quickLift hub file and populate trackNames with the set of + * track names already present. Also returns the highest priority value seen + * (0 if the file has no track stanzas yet) so new tracks can be appended after + * existing ones. */ +{ +unsigned maxPriority = 0; +struct lineFile *lf = lineFileMayOpen(filename, TRUE); +if (lf != NULL) + { + char *line; + while (lineFileNextReal(lf, &line)) + { + if (startsWithWord("track", line)) + { + char *name = skipLeadingSpaces(line + 5); + if (isNotEmpty(name)) + hashStoreName(trackNames, cloneString(firstWordInLine(name))); + } + else if (startsWithWord("priority", line)) + { + char *val = skipLeadingSpaces(line + 8); + if (isNotEmpty(val)) + { + unsigned p = sqlUnsigned(firstWordInLine(val)); + if (p > maxPriority) + maxPriority = p; + } + } + } + lineFileClose(&lf); + } +if (retMaxPriority != NULL) + *retMaxPriority = maxPriority; +} + static int cmpPriority(const void *va, const void *vb) /* Compare to sort based on priority; use shortLabel as secondary sort key. */ { const struct trackDb *a = *((struct trackDb **)va); const struct trackDb *b = *((struct trackDb **)vb); float dif = 0; dif = a->groupPriority - b->groupPriority; if (dif == 0) dif = a->priority - b->priority; if (dif < 0) return -1; else if (dif == 0.0) /* secondary sort on label */ return strcasecmp(a->shortLabel, b->shortLabel); else return 1; } char *trackHubBuild(char *db, struct cart *cart, struct dyString *visDy, struct trackDb **badList) -/* Build a track hub using trackDb and the cart. */ +/* Build a track hub using trackDb and the cart. If a hub file already exists + * for db (i.e. earlier quickLift work in the same session), append new track + * stanzas to it instead of overwriting, and skip tracks that are already in + * the file. */ { struct trackDb *tdbList, *tdb; struct grp *grpList; cartTrackDbInit(cart, &tdbList, &grpList, FALSE); struct hash *groupHash = newHash(0); struct grp *grp; for(grp = grpList; grp; grp = grp->next) hashAdd(groupHash, grp->name, grp); for(tdb = tdbList; tdb; tdb = tdb->next) { grp = hashFindVal(groupHash, tdb->grp); tdb->groupPriority = grp->priority; } slSort(&tdbList, cmpPriority); char *filename = getHubName(cart, db); -FILE *f = mustOpen(filename, "w"); +struct hash *existingTracks = newHash(8); +unsigned maxPriority = 0; +readExistingHubTracks(filename, existingTracks, &maxPriority); +boolean hubExists = (hashNumEntries(existingTracks) > 0); + +FILE *f = mustOpen(filename, hubExists ? "a" : "w"); chmod(filename, 0666); +if (!hubExists) outHubHeader(f, trackHubSkipHubName(db)); -walkTree(f, db, cart, tdbList, visDy, badList); +walkTree(f, db, cart, tdbList, visDy, badList, existingTracks, maxPriority + 1); fclose(f); return cloneString(filename); } struct grp *trackHubGetGrps() /* Get the groups defined by attached track hubs. */ { return trackHubGrps; } struct trackDb *trackHubAddTracksGenome(struct trackHubGenome *hubGenome) /* Load up stuff from data hub and return list. */ { /* Load trackDb.ra file and make it into proper trackDb tree */