547bbe0a4ad38df6b0b7a511d6a2ce24b3625fe7
max
  Thu Sep 17 05:08:41 2026 -0700
hubtools import session: keep the track order of the session, refs #34405

#Preview2 week - bugs introduced now will need a build patch to fix
The custom track backup archive has one file per track and no ordering
information: hgSession wrote the files into a directory and tar walks it in
readdir order, which is not the order they were written. hubtools globbed
them, and a hub track without a priority is shown in alphabetical order of
its label, so an imported session came out sorted by label.

hgSession now writes the priority hgTracks orders the tracks by into each
track line of the archive, either the one the browser assigned when it
loaded the track or the cart variable that a drag-and-drop reorder left
behind. A track line that names its own priority is left alone. hubtools
sorts on that priority and numbers the hub stanzas, continuing the count
across assemblies so two genome stanzas cannot interleave. Archives made
before this have no priority and fall back to file name order.

diff --git src/utils/hubtools/hubtools src/utils/hubtools/hubtools
index ccf16f9b8ed..51a83ad9406 100755
--- src/utils/hubtools/hubtools
+++ src/utils/hubtools/hubtools
@@ -3543,66 +3543,85 @@
         tdb["type"] = "bigBed 6+3"
         tdb["spectrum"] = "on"
 
     else:
         errAbort("No support yet for track type '%s'. Please contact us." % trackType)
 
     tdb["bigDataUrl"] = basename(outFname)
     return tdb
 
 def relBigDataUrl(urlPrefix, fname):
     " a bigDataUrl relative to the directory that holds hub.txt "
     if not urlPrefix:
         return fname
     return urlPrefix + "/" + fname
 
+def ctSortByPriority(tdbData):
+    """ order the custom tracks of one assembly the way the session shows them. The
+    archive has one file per track and the tar was made from a directory listing, so the
+    file order is meaningless; the order lives in the priority of the track line, which
+    hgSession writes into every track of the archive. Archives made before it did that
+    have no priority, for those keep the file name order, which is at least stable. """
+    def sortKey(item):
+        fname, tdb = item
+        prio = tdb.get("priority", "")
+        # a track line without a priority sorts after all of the numbered ones
+        return (0, float(prio), fname) if isNumber(prio) else (1, 0.0, fname)
+
+    return sorted(tdbData.items(), key=sortKey)
+
 def convCtDb(db, inDir, outDir, urlPrefix, ofh, doDownload, startIdx=0):
     """ convert one db part of a track archive: append a 'genome' stanza and one stanza
     per custom track to the already-open hub.txt handle ofh, and put converted or
     downloaded data files into outDir. bigDataUrls of local files are prefixed with
     urlPrefix, so hub.txt can live in the directory above the data files.
     Track names are numbered from startIdx: a single-file hub has one track namespace
     for all of its 'genome' stanzas, so the numbering has to continue across assemblies.
     Returns (list of (url, localFname) still to download, number of tracks written).
     """
     findGlob = join(inDir, "*.ct")
     inFnames = glob.glob(findGlob)
     if len(inFnames)==0:
         logging.info("No *.ct files found in %s" % findGlob)
         return [], 0
 
     tdbData = readTrackLines(inFnames)
 
     writeGenomeStanza(ofh, db)
 
     getUrlsFnames = []
     doneFnames = set()
     tdbIdx = startIdx
-    for fname, tdb in tdbData.items():
+    for fname, tdb in ctSortByPriority(tdbData):
         tdb["shortLabel"] = tdb["name"]
 
         tdbIdx += 1
         # custom track names can include spaces, spec characters, etc. Strip all those
         # append a number to make sure that the result is unique and a legal track name
         track = tdb["name"]
         track = makeLegalTrackName(track)+"_"+str(tdbIdx)
         tdb["track"] = track
         del tdb["name"]
 
         # the description= attribute is optional in a custom track line
         tdb["longLabel"] = tdb.pop("description", tdb["shortLabel"])
 
+        # hgTracks shows hub tracks that have no priority in alphabetical order of their
+        # label, so renumber the tracks here to keep the order of the session. The
+        # counter runs across assemblies, so two 'genome' stanzas cannot interleave.
+        tdb["priority"] = str(tdbIdx)
+
         normalizeOnOff(tdb)
 
         if "bigDataUrl" not in tdb:
             makedirs(outDir)
             textFname = join(outDir, tdb["track"]+".txt")
             stripFirstLine(fname, textFname)
 
             tdb = convertTextToBin(db, textFname, tdb, outDir)
             tdb["bigDataUrl"] = relBigDataUrl(urlPrefix, tdb["bigDataUrl"])
 
             os.remove(textFname)
         else:
             url = tdb["bigDataUrl"]
             if doDownload:
                 uniqueFname = basename(url)