7ba5c812bda048b28ade940e0030b8000a02ae4b
chmalee
  Mon Aug 10 11:58:51 2026 -0700
hubSpace: key rows on location so two hubs can hold the same file name, refs #37964

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

diff --git src/hg/hgHubConnect/hooks/pre-finish.c src/hg/hgHubConnect/hooks/pre-finish.c
index 4443d1810c2..d41b1aebe6f 100644
--- src/hg/hgHubConnect/hooks/pre-finish.c
+++ src/hg/hgHubConnect/hooks/pre-finish.c
@@ -196,86 +196,84 @@
                 row->location = canonicalPath;
             else
                 {
                 // all upload data should have been received and thus the realpath
                 // should not fail, but just in case, put something valid here
                 row->location = tusFile;
                 }
             row->md5sum = md5HexForFile(row->location);
             row->parentDir = encodedParentDir ? encodedParentDir : "";
             // Derive hubType server-side; never trust the client's hubType.
             // A 2bit always promotes its hub to assembly. Otherwise inherit
             // the existing hub's type, defaulting to trackHub.
             // both lookups below are about the hub as a whole, whose row and hub.txt
             // live at the top level, so use the hub component of parentDir
             char *parentDirForCheck = encodedParentDir ? hubRootFromParentDir(encodedParentDir) : "";
+            char *hubDir = encodedParentDir ?
+                hubPathFromParentDir(encodedParentDir, userDataDir) : NULL;
             if (sameOk(fileType, "2bit"))
                 row->hubType = "assemblyHub";
             else
                 {
                 char *existingType = existingHubTypeForDir(userName, parentDirForCheck);
                 row->hubType = existingType ? existingType : "trackHub";
                 }
             char *batchHasHubTxtStr = jsonQueryString(req, "", "Event.Upload.MetaData.batchHasHubTxt", NULL);
             boolean batchHasHubTxt = sameOk(batchHasHubTxtStr, "true");
-            boolean userOwnNamedHubTxt = userHasOwnNamedHubTxtInDir(userName, parentDirForCheck);
+            boolean userOwnNamedHubTxt = userHasOwnNamedHubTxtInDir(userName, parentDirForCheck, hubDir);
             boolean userAuth = batchHasHubTxt || userOwnNamedHubTxt;
             boolean isHubTxt = sameOk(fileType, "hub.txt");
             boolean isTwoBit = sameOk(fileType, "2bit");
 
             // Serialize hub.txt read-modify-write across parallel pre-finish
             // processes for the same hub. flock is held for the entire
             // decision + action so writeHubText's fileExists check and the
             // upgrade's read-rewrite are atomic with respect to siblings.
             // Lock the directory the hub.txt is in, so uploads into different
             // subdirectories of one hub still serialize against each other.
             int hubLockFd = -1;
-            if (encodedParentDir)
-                {
-                char *hubDir = hubPathFromParentDir(encodedParentDir, userDataDir);
+            if (hubDir)
                 hubLockFd = lockHubDir(hubDir);
-                freeMem(hubDir);
-                }
             if (!isHubToolsUpload && !isHubTxt)
                 {
                 if (!userAuth)
                     {
                     if (isTwoBit)
                         {
                         // createNewTempHubForUpload is a no-op when the hub.txt and its
                         // row are already there, and it backfills the row when they are not
                         createNewTempHubForUpload(reqId, row, userDataDir);
-                        upgradeExistingHubToAssembly(row, userDataDir, encodedParentDir);
+                        upgradeExistingHubToAssembly(row, userDataDir);
                         }
                     else
                         createNewTempHubForUpload(reqId, row, userDataDir);
                     }
                 else if (isTwoBit)
                     {
                     // user's hub.txt is authoritative; just flip rows to assemblyHub.
-                    upgradeExistingHubToAssembly(row, userDataDir, encodedParentDir);
+                    upgradeExistingHubToAssembly(row, userDataDir);
                     }
                 }
             // still under the hub lock: makeParentDirRows checks for a row and then
             // inserts it, so two uploads to one hub would otherwise both insert the
             // same directory row
             // first make the parentDir rows
             // the directory rows carry the upload's own timestamp. row->lastModified
             // holds it as a GMT clock string, which sqlDateToUnixTime would read as
             // local time, so pass the seconds directly
             makeParentDirRows(row->userName, lastModified, row->db, row->parentDir, userDataDir, row->hubType);
-            row->parentDir = encodedParentDir ? hubNameFromPath(encodedParentDir) : "";
+            row->parentDir = encodedParentDir ? hubLeafFromPath(encodedParentDir) : "";
             addHubSpaceRowForFile(row);
             unlockHubDir(hubLockFd);
             fprintf(stderr, "added hubSpace row for file '%s'\n", fileName);
             fflush(stderr);
 
             // Send the client the hub as it now stands, so it can show the new file,
             // its directories and the hub.txt. The upload has already succeeded at
             // this point, so catch errors here rather than let them reject it: the
             // worst case is a client that shows nothing new until the page is reloaded
             struct errCatch *respCatch = errCatchNew(0);
             if (errCatchStart(respCatch))
                 {
                 if (encodedParentDir)
                     {
                     char *hubName = hubRootFromParentDir(encodedParentDir);