37e40ae31e02d615256bb9af43468ec2188afd24
braney
  Fri Feb 24 15:19:01 2023 -0800
make recommended track sets merge with current cart instead of
over-writing it.  This is under hg.conf control (mergeRecommended=on)

diff --git src/hg/lib/cart.c src/hg/lib/cart.c
index ff556ab..2f03f02 100644
--- src/hg/lib/cart.c
+++ src/hg/lib/cart.c
@@ -536,38 +536,39 @@
 
 static void hashEmpty(struct hash *hash)
 /* Remove everything from hash. */
 {
 struct hashEl *hel, *helList = hashElListHash(hash);
 for (hel = helList;  hel != NULL;  hel = hel->next)
     {
     freez(&(hel->val));
     hashRemove(hash, hel->name);
     }
 hashElFreeList(&helList);
 assert(hashNumEntries(hash) == 0);
 }
 
 #ifndef GBROWSE
-void cartLoadUserSession(struct sqlConnection *conn, char *sessionOwner,
+void cartLoadUserSessionExt(struct sqlConnection *conn, char *sessionOwner,
 			 char *sessionName, struct cart *cart,
-			 struct hash *oldVars, char *actionVar)
+			 struct hash *oldVars, char *actionVar, boolean merge)
 /* If permitted, load the contents of the given user's session, and then
  * reload the CGI settings (to support override of session settings).
  * If non-NULL, oldVars will contain values overloaded when reloading CGI.
  * If non-NULL, actionVar is a cartRemove wildcard string specifying the
- * CGI action variable that sent us here. */
+ * CGI action variable that sent us here. 
+ * If merge is TRUE, then don't clear the cart first. */
 {
 struct sqlResult *sr = NULL;
 char **row = NULL;
 char *userName = wikiLinkUserName();
 char *encSessionName = cgiEncodeFull(sessionName);
 char *encSessionOwner = cgiEncodeFull(sessionOwner);
 char query[512];
 
 if (isEmpty(sessionOwner))
     errAbort("Please go back and enter a wiki user name for this session.");
 if (isEmpty(sessionName))
     errAbort("Please go back and enter a session name to load.");
 
 sqlSafef(query, sizeof(query), "SELECT shared, contents FROM %s "
       "WHERE userName = '%s' AND sessionName = '%s';",
@@ -575,56 +576,71 @@
 sr = sqlGetResult(conn, query);
 if ((row = sqlNextRow(sr)) != NULL)
     {
     boolean shared = atoi(row[0]);
     if (shared ||
 	(userName && sameString(sessionOwner, userName)))
 	{
 	char *sessionVar = cartSessionVarName();
 	char *hgsid = cartSessionId(cart);
     char *sessionTableString = cartOptionalString(cart, hgSessionTableState);
     sessionTableString = cloneString(sessionTableString);
     char *pubSessionsTableString = cartOptionalString(cart, hgPublicSessionsTableState);
     pubSessionsTableString = cloneString(pubSessionsTableString);
 	struct sqlConnection *conn2 = hConnectCentral();
 	sessionTouchLastUse(conn2, encSessionOwner, encSessionName);
+        if (!merge)
             cartRemoveLike(cart, "*");
 	cartParseOverHash(cart, row[1]);
 	cartSetString(cart, sessionVar, hgsid);
 	if (sessionTableString != NULL)
 	    cartSetString(cart, hgSessionTableState, sessionTableString);
 	if (pubSessionsTableString != NULL)
 	    cartSetString(cart, hgPublicSessionsTableState, pubSessionsTableString);
 	if (oldVars)
 	    hashEmpty(oldVars);
 	/* Overload settings explicitly passed in via CGI (except for the
 	 * command that sent us here): */
 	loadCgiOverHash(cart, oldVars);
 	if (isNotEmpty(actionVar))
 	    cartRemove(cart, actionVar);
 	hDisconnectCentral(&conn2);
 	}
     else
 	errAbort("Sharing has not been enabled for user %s's session %s.",
 		 sessionOwner, sessionName);
     }
 else
     errAbort("Could not find session %s for user %s.",
 	     sessionName, sessionOwner);
 sqlFreeResult(&sr);
 freeMem(encSessionName);
 }
+
+void cartLoadUserSession(struct sqlConnection *conn, char *sessionOwner,
+			 char *sessionName, struct cart *cart,
+			 struct hash *oldVars, char *actionVar)
+/* If permitted, load the contents of the given user's session, and then
+ * reload the CGI settings (to support override of session settings).
+ * If non-NULL, oldVars will contain values overloaded when reloading CGI.
+ * If non-NULL, actionVar is a cartRemove wildcard string specifying the
+ * CGI action variable that sent us here. */
+{
+cartLoadUserSessionExt(conn, sessionOwner,
+			 sessionName, cart,
+			 oldVars, actionVar, FALSE);
+}
 #endif /* GBROWSE */
 
 boolean containsNonPrintable(char *string)
 /* Return TRUE if string contains non-ascii printable character(s). */
 {
 if (isEmpty(string))
     return FALSE;
 boolean hasNonPrintable = FALSE;
 int i;
 for (i = 0;  string[i] != '\0';  i++)
     {
     if ((string[i] < 32 || string[i] > 126) && string[i] != '\t')
         {
         hasNonPrintable = TRUE;
         break;
@@ -1360,33 +1376,34 @@
 
 // I think this is the place to justify old and new values
 cartJustify(cart, oldVars);
 
 #ifndef GBROWSE
 /* If some CGI other than hgSession been passed hgSession loading instructions,
  * apply those to cart before we do anything else.  (If this is hgSession,
  * let it handle the settings so it can display feedback to the user.) */
 boolean didSessionLoad = FALSE;
 if (! (cgiScriptName() && endsWith(cgiScriptName(), "hgSession")))
     {
     if (cartVarExists(cart, hgsDoOtherUser))
 	{
 	char *otherUser = cartString(cart, hgsOtherUserName);
 	char *sessionName = cartString(cart, hgsOtherUserSessionName);
+	boolean mergeCart = cartUsualBoolean(cart, hgsMergeCart, FALSE);
 	struct sqlConnection *conn2 = hConnectCentral();
-	cartLoadUserSession(conn2, otherUser, sessionName, cart,
-			    oldVars, hgsDoOtherUser);
+	cartLoadUserSessionExt(conn2, otherUser, sessionName, cart,
+			    oldVars, hgsDoOtherUser, mergeCart);
 	hDisconnectCentral(&conn2);
 	cartTrace(cart, "after cartLUS", conn);
 	didSessionLoad = TRUE;
 	}
     else if (cartVarExists(cart, hgsDoLoadUrl))
 	{
 	char *url = cartString(cart, hgsLoadUrlName);
 	struct lineFile *lf = netLineFileOpen(url);
         struct dyString *dyMessage = dyStringNew(0);
 	boolean ok = cartLoadSettingsFromUserInput(lf, cart, oldVars, hgsDoLoadUrl, dyMessage);
 	lineFileClose(&lf);
 	cartTrace(cart, "after cartLS", conn);
         if (! ok)
             {
             warn("Unable to load session file: %s", dyMessage->string);