Index: src/branch.c ================================================================== --- src/branch.c +++ src/branch.c @@ -344,22 +344,241 @@ " AND ox.rid=ix.rid)", TAG_BRANCH, zBrName, TAG_CLOSED ); } +/* +** Internal helper for branch_cmd_close() and friends. Adds a row to +** the to the brcmdtag TEMP table, initializing that table if needed, +** holding a pending tag for the given blob.rid (which is assumed to +** be valid). zTag must be a fully-formed tag name, including the +** (+,-,*) prefix character. +** +*/ +static void branch_cmd_tag_add(int rid, const char *zTag){ + static int once = 0; + assert(zTag && ('+'==zTag[0] || '-'==zTag[0] || '*'==zTag[0])); + if(0==once++){ + db_multi_exec("CREATE TEMP TABLE brcmdtag(" + "rid INTEGER UNIQUE ON CONFLICT IGNORE," + "tag TEXT NOT NULL" + ")"); + } + db_multi_exec("INSERT INTO brcmdtag(rid,tag) VALUES(%d,%Q)", + rid, zTag); +} + +/* +** Internal helper for branch_cmd_close() and friends. Creates and +** saves a control artifact of tag changes stored via +** branch_cmd_tag_add(). Fails fatally on error, returns 0 if it saves +** an artifact, and a negative value if it does not save anything +** because no tags were queued up. A positive return value is reserved +** for potential future semantics. +** +** This function asserts that a transaction is underway and it ends +** the transaction, committing or rolling back, as appropriate. +*/ +static int branch_cmd_tag_finalize(int fDryRun /* roll back if true */, + int fVerbose /* output extra info */, + const char *zDateOvrd /* --date-override */, + const char *zUserOvrd /* --user-override */){ + int nTags = 0; + Stmt q = empty_Stmt; + Blob manifest = empty_blob; + int doRollback = fDryRun!=0; + + assert(db_transaction_nesting_depth() > 0); + if(!db_table_exists("temp","brcmdtag")){ + fossil_warning("No tags added - nothing to do."); + db_end_transaction(1); + return -1; + } + db_prepare(&q, "SELECT b.uuid, t.tag " + "FROM blob b, brcmdtag t " + "WHERE b.rid=t.rid " + "ORDER BY t.tag, b.uuid"); + blob_appendf(&manifest, "D %z\n", + date_in_standard_format( zDateOvrd ? zDateOvrd : "now")); + while(SQLITE_ROW==db_step(&q)){ + const char * zHash = db_column_text(&q, 0); + const char * zTag = db_column_text(&q, 1); + blob_appendf(&manifest, "T %s %s\n", zTag, zHash); + ++nTags; + } + if(!nTags){ + fossil_warning("No tags added - nothing to do."); + db_end_transaction(1); + blob_reset(&manifest); + return -1; + } + user_select(); + blob_appendf(&manifest, "U %F\n", zUserOvrd ? zUserOvrd : login_name()); + { /* Z-card and save artifact */ + int newRid; + Blob cksum = empty_blob; + md5sum_blob(&manifest, &cksum); + blob_appendf(&manifest, "Z %b\n", &cksum); + blob_reset(&cksum); + if(fDryRun && fVerbose){ + fossil_print("Dry-run mode: will roll back new artifact:\n%b", + &manifest); + /* Run through the saving steps, though, noting that doing so + ** will clear out &manifest, which is why we output it here + ** instead of after saving. */ + } + newRid = content_put(&manifest); + if(0==newRid){ + fossil_fatal("Problem saving new artifact: %s\n%b", + g.zErrMsg, &manifest); + }else if(manifest_crosslink(newRid, &manifest, 0)==0){ + fossil_fatal("Crosslinking error: %s", g.zErrMsg); + } + fossil_print("Saved new control artifact %z (RID %d).\n", + rid_to_uuid(newRid), newRid); + db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", newRid); + if(fDryRun){ + fossil_print("Dry-run mode: rolling back new artifact.\n"); + assert(0!=doRollback); + } + } + db_multi_exec("DROP TABLE brcmdtag"); + blob_reset(&manifest); + db_end_transaction(doRollback); + return 0; +} + +/* +** Internal helper for branch_cmd_close() and friends. zName is a +** symbolic checkin name. Returns the blob.rid of the checkin or fails +** fatally if the name does not resolve unambiguously. If zUuid is +** not NULL, *zUuid is set to the resolved blob.uuid and must be freed +** by the caller via fossil_free(). +*/ +static int branch_resolve_name(char const *zName, char **zUuid){ + const int rid = name_to_uuid2(zName, "ci", zUuid); + if(0==rid){ + fossil_fatal("Cannot resolve name: %s", zName); + }else if(rid<0){ + fossil_fatal("Ambiguous name: %s", zName); + } + return rid; +} + +/* +** Implementation of (branch hide/unhide) subcommands. nStartAtArg is +** the g.argv index to start reading branch/checkin names. fHide is +** true for hiding, false for unhiding. Fails fatally on error. +*/ +static void branch_cmd_hide(int nStartAtArg, int fHide){ + int argPos = nStartAtArg; /* g.argv pos with first branch/ci name */ + char * zUuid = 0; /* Resolved branch UUID. */ + const int fVerbose = find_option("verbose","v",0)!=0; + const int fDryRun = find_option("dry-run","n",0)!=0; + const char *zDateOvrd = find_option("date-override",0,1); + const char *zUserOvrd = find_option("user-override",0,1); + + verify_all_options(); + db_begin_transaction(); + for( ; argPos < g.argc; fossil_free(zUuid), ++argPos ){ + const char * zName = g.argv[argPos]; + const int rid = branch_resolve_name(zName, &zUuid); + const int isHidden = tag_has(rid, TAG_HIDDEN); + /* Potential TODO: check for existing 'hidden' flag and skip this + ** entry if it already has (if fHide) or does not have (if !fHide) + ** that tag. FWIW, /ci_edit does not do so. */ + if(fHide && isHidden){ + fossil_warning("Skipping hidden checkin %s: %s.", zName, zUuid); + continue; + }else if(!fHide && !isHidden){ + fossil_warning("Skipping non-hidden checkin %s: %s.", zName, zUuid); + continue; + } + branch_cmd_tag_add(rid, fHide ? "*hidden" : "-hidden"); + if(fVerbose!=0){ + fossil_print("%s checkin [%s] %s\n", + fHide ? "Hiding" : "Unhiding", + zName, zUuid); + } + } + branch_cmd_tag_finalize(fDryRun, fVerbose, zDateOvrd, zUserOvrd); +} + +/* +** Implementation of (branch close|reopen) subcommands. nStartAtArg is +** the g.argv index to start reading branch/checkin names. The given +** checkins are closed if fClose is true, else their "closed" tag (if +** any) is cancelled. Fails fatally on error. +*/ +static void branch_cmd_close(int nStartAtArg, int fClose){ + int argPos = nStartAtArg; /* g.argv pos with first branch name */ + char * zUuid = 0; /* Resolved branch UUID. */ + const int fVerbose = find_option("verbose","v",0)!=0; + const int fDryRun = find_option("dry-run","n",0)!=0; + const char *zDateOvrd = find_option("date-override",0,1); + const char *zUserOvrd = find_option("user-override",0,1); + + verify_all_options(); + db_begin_transaction(); + for( ; argPos < g.argc; fossil_free(zUuid), ++argPos ){ + const char * zName = g.argv[argPos]; + const int rid = branch_resolve_name(zName, &zUuid); + const int isClosed = leaf_is_closed(rid); + if(!is_a_leaf(rid)){ + /* This behaviour is different from /ci_edit closing, where + ** is_a_leaf() adds a "+" tag and !is_a_leaf() adds a "*" + ** tag. We might want to change this to match for consistency's + ** sake, but it currently seems unnecessary to close/re-open a + ** non-leaf. */ + fossil_warning("Skipping non-leaf [%s] %s", zName, zUuid); + continue; + }else if(fClose && isClosed){ + fossil_warning("Skipping closed leaf [%s] %s", zName, zUuid); + continue; + }else if(!fClose && !isClosed){ + fossil_warning("Skipping non-closed leaf [%s] %s", zName, zUuid); + continue; + } + branch_cmd_tag_add(rid, fClose ? "+closed" : "-closed"); + if(fVerbose!=0){ + fossil_print("%s branch [%s] %s\n", + fClose ? "Closing" : "Re-opening", + zName, zUuid); + } + } + branch_cmd_tag_finalize(fDryRun, fVerbose, zDateOvrd, zUserOvrd); +} /* ** COMMAND: branch ** ** Usage: %fossil branch SUBCOMMAND ... ?OPTIONS? ** ** Run various subcommands to manage branches of the open repository or ** of the repository identified by the -R or --repository option. ** +** > fossil branch close|reopen ?OPTIONS? BRANCH-NAME ?...BRANCH-NAMES? +** +** Adds or cancels the "closed" tag to one or more branches. +** It accepts arbitrary unambiguous symbolic names but +** will only resolve checkin names and skips any which resolve +** to non-leaf checkins. Options: +** -n|--dry-run do not commit changes and dump artifact +** to stdout +** -v|--verbose output more information +** --date-override DATE DATE to use instead of 'now' +** --user-override USER USER to use instead of the current default +** ** > fossil branch current ** ** Print the name of the branch for the current check-out +** +** > fossil branch hide|unhide ?OPTIONS? BRANCH-NAME ?...BRANCH-NAMES? +** +** Adds or cancels the "hidden" tag for the specified branches or +** or checkin IDs. Accepts the same options as the close +** subcommand. ** ** > fossil branch info BRANCH-NAME ** ** Print information about a branch ** @@ -452,13 +671,33 @@ fossil_print("%s%s\n", (isCur ? "* " : " "), zBr); } db_finalize(&q); }else if( strncmp(zCmd,"new",n)==0 ){ branch_new(); + }else if( strncmp(zCmd,"close",5)==0 ){ + if(g.argc<4){ + usage("branch close branch-name(s)..."); + } + branch_cmd_close(3, 1); + }else if( strncmp(zCmd,"reopen",6)==0 ){ + if(g.argc<4){ + usage("branch reopen branch-name(s)..."); + } + branch_cmd_close(3, 0); + }else if( strncmp(zCmd,"hide",4)==0 ){ + if(g.argc<4){ + usage("branch hide branch-name(s)..."); + } + branch_cmd_hide(3,1); + }else if( strncmp(zCmd,"unhide",6)==0 ){ + if(g.argc<4){ + usage("branch unhide branch-name(s)..."); + } + branch_cmd_hide(3,0); }else{ fossil_fatal("branch subcommand should be one of: " - "current info list ls new"); + "close current hide info list ls new reopen unhide"); } } /* ** This is the new-style branch-list page that shows the branch names Index: src/tag.c ================================================================== --- src/tag.c +++ src/tag.c @@ -889,5 +889,19 @@ www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, 0); db_finalize(&q); @
style_finish_page(); } + +/* +** Returns true if the given blob.rid value has the given tag ID +** applied to it, else false. +*/ +int tag_has(int rid, int tagId){ + return db_exists( + "SELECT tag.tagid FROM tagxref, tag" + " WHERE tagxref.rid=%d AND tagtype>0 " + " AND tag.tagid=%d" + " AND tagxref.tagid=tag.tagid", + rid, tagId + ); +} Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -18,10 +18,12 @@ the current page and list URLs suitable for pasting them into the page. * Add the --no-http-compression option to [/help?cmd=sync|fossil sync] and similar. * Print total payload bytes on a [/help?cmd=sync|fossil sync] when using the --verbose option. + * Add the close, hide, and unhide subcommands + to [/help?cmd=branch|the branch command].

Changes for Version 2.16 (2021-07-02)

* Security: Fix the client-side TLS so that it verifies that the server hostname matches its certificate.