| | @@ -344,22 +344,241 @@ |
| 344 | 344 | " AND ox.rid=ix.rid)", |
| 345 | 345 | TAG_BRANCH, zBrName, TAG_CLOSED |
| 346 | 346 | ); |
| 347 | 347 | } |
| 348 | 348 | |
| 349 | +/* |
| 350 | +** Internal helper for branch_cmd_close() and friends. Adds a row to |
| 351 | +** the to the brcmdtag TEMP table, initializing that table if needed, |
| 352 | +** holding a pending tag for the given blob.rid (which is assumed to |
| 353 | +** be valid). zTag must be a fully-formed tag name, including the |
| 354 | +** (+,-,*) prefix character. |
| 355 | +** |
| 356 | +*/ |
| 357 | +static void branch_cmd_tag_add(int rid, const char *zTag){ |
| 358 | + static int once = 0; |
| 359 | + assert(zTag && ('+'==zTag[0] || '-'==zTag[0] || '*'==zTag[0])); |
| 360 | + if(0==once++){ |
| 361 | + db_multi_exec("CREATE TEMP TABLE brcmdtag(" |
| 362 | + "rid INTEGER UNIQUE ON CONFLICT IGNORE," |
| 363 | + "tag TEXT NOT NULL" |
| 364 | + ")"); |
| 365 | + } |
| 366 | + db_multi_exec("INSERT INTO brcmdtag(rid,tag) VALUES(%d,%Q)", |
| 367 | + rid, zTag); |
| 368 | +} |
| 369 | + |
| 370 | +/* |
| 371 | +** Internal helper for branch_cmd_close() and friends. Creates and |
| 372 | +** saves a control artifact of tag changes stored via |
| 373 | +** branch_cmd_tag_add(). Fails fatally on error, returns 0 if it saves |
| 374 | +** an artifact, and a negative value if it does not save anything |
| 375 | +** because no tags were queued up. A positive return value is reserved |
| 376 | +** for potential future semantics. |
| 377 | +** |
| 378 | +** This function asserts that a transaction is underway and it ends |
| 379 | +** the transaction, committing or rolling back, as appropriate. |
| 380 | +*/ |
| 381 | +static int branch_cmd_tag_finalize(int fDryRun /* roll back if true */, |
| 382 | + int fVerbose /* output extra info */, |
| 383 | + const char *zDateOvrd /* --date-override */, |
| 384 | + const char *zUserOvrd /* --user-override */){ |
| 385 | + int nTags = 0; |
| 386 | + Stmt q = empty_Stmt; |
| 387 | + Blob manifest = empty_blob; |
| 388 | + int doRollback = fDryRun!=0; |
| 389 | + |
| 390 | + assert(db_transaction_nesting_depth() > 0); |
| 391 | + if(!db_table_exists("temp","brcmdtag")){ |
| 392 | + fossil_warning("No tags added - nothing to do."); |
| 393 | + db_end_transaction(1); |
| 394 | + return -1; |
| 395 | + } |
| 396 | + db_prepare(&q, "SELECT b.uuid, t.tag " |
| 397 | + "FROM blob b, brcmdtag t " |
| 398 | + "WHERE b.rid=t.rid " |
| 399 | + "ORDER BY t.tag, b.uuid"); |
| 400 | + blob_appendf(&manifest, "D %z\n", |
| 401 | + date_in_standard_format( zDateOvrd ? zDateOvrd : "now")); |
| 402 | + while(SQLITE_ROW==db_step(&q)){ |
| 403 | + const char * zHash = db_column_text(&q, 0); |
| 404 | + const char * zTag = db_column_text(&q, 1); |
| 405 | + blob_appendf(&manifest, "T %s %s\n", zTag, zHash); |
| 406 | + ++nTags; |
| 407 | + } |
| 408 | + if(!nTags){ |
| 409 | + fossil_warning("No tags added - nothing to do."); |
| 410 | + db_end_transaction(1); |
| 411 | + blob_reset(&manifest); |
| 412 | + return -1; |
| 413 | + } |
| 414 | + user_select(); |
| 415 | + blob_appendf(&manifest, "U %F\n", zUserOvrd ? zUserOvrd : login_name()); |
| 416 | + { /* Z-card and save artifact */ |
| 417 | + int newRid; |
| 418 | + Blob cksum = empty_blob; |
| 419 | + md5sum_blob(&manifest, &cksum); |
| 420 | + blob_appendf(&manifest, "Z %b\n", &cksum); |
| 421 | + blob_reset(&cksum); |
| 422 | + if(fDryRun && fVerbose){ |
| 423 | + fossil_print("Dry-run mode: will roll back new artifact:\n%b", |
| 424 | + &manifest); |
| 425 | + /* Run through the saving steps, though, noting that doing so |
| 426 | + ** will clear out &manifest, which is why we output it here |
| 427 | + ** instead of after saving. */ |
| 428 | + } |
| 429 | + newRid = content_put(&manifest); |
| 430 | + if(0==newRid){ |
| 431 | + fossil_fatal("Problem saving new artifact: %s\n%b", |
| 432 | + g.zErrMsg, &manifest); |
| 433 | + }else if(manifest_crosslink(newRid, &manifest, 0)==0){ |
| 434 | + fossil_fatal("Crosslinking error: %s", g.zErrMsg); |
| 435 | + } |
| 436 | + fossil_print("Saved new control artifact %z (RID %d).\n", |
| 437 | + rid_to_uuid(newRid), newRid); |
| 438 | + db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", newRid); |
| 439 | + if(fDryRun){ |
| 440 | + fossil_print("Dry-run mode: rolling back new artifact.\n"); |
| 441 | + assert(0!=doRollback); |
| 442 | + } |
| 443 | + } |
| 444 | + db_multi_exec("DROP TABLE brcmdtag"); |
| 445 | + blob_reset(&manifest); |
| 446 | + db_end_transaction(doRollback); |
| 447 | + return 0; |
| 448 | +} |
| 449 | + |
| 450 | +/* |
| 451 | +** Internal helper for branch_cmd_close() and friends. zName is a |
| 452 | +** symbolic checkin name. Returns the blob.rid of the checkin or fails |
| 453 | +** fatally if the name does not resolve unambiguously. If zUuid is |
| 454 | +** not NULL, *zUuid is set to the resolved blob.uuid and must be freed |
| 455 | +** by the caller via fossil_free(). |
| 456 | +*/ |
| 457 | +static int branch_resolve_name(char const *zName, char **zUuid){ |
| 458 | + const int rid = name_to_uuid2(zName, "ci", zUuid); |
| 459 | + if(0==rid){ |
| 460 | + fossil_fatal("Cannot resolve name: %s", zName); |
| 461 | + }else if(rid<0){ |
| 462 | + fossil_fatal("Ambiguous name: %s", zName); |
| 463 | + } |
| 464 | + return rid; |
| 465 | +} |
| 466 | + |
| 467 | +/* |
| 468 | +** Implementation of (branch hide/unhide) subcommands. nStartAtArg is |
| 469 | +** the g.argv index to start reading branch/checkin names. fHide is |
| 470 | +** true for hiding, false for unhiding. Fails fatally on error. |
| 471 | +*/ |
| 472 | +static void branch_cmd_hide(int nStartAtArg, int fHide){ |
| 473 | + int argPos = nStartAtArg; /* g.argv pos with first branch/ci name */ |
| 474 | + char * zUuid = 0; /* Resolved branch UUID. */ |
| 475 | + const int fVerbose = find_option("verbose","v",0)!=0; |
| 476 | + const int fDryRun = find_option("dry-run","n",0)!=0; |
| 477 | + const char *zDateOvrd = find_option("date-override",0,1); |
| 478 | + const char *zUserOvrd = find_option("user-override",0,1); |
| 479 | + |
| 480 | + verify_all_options(); |
| 481 | + db_begin_transaction(); |
| 482 | + for( ; argPos < g.argc; fossil_free(zUuid), ++argPos ){ |
| 483 | + const char * zName = g.argv[argPos]; |
| 484 | + const int rid = branch_resolve_name(zName, &zUuid); |
| 485 | + const int isHidden = rid_has_tag(rid, TAG_HIDDEN); |
| 486 | + /* Potential TODO: check for existing 'hidden' flag and skip this |
| 487 | + ** entry if it already has (if fHide) or does not have (if !fHide) |
| 488 | + ** that tag. FWIW, /ci_edit does not do so. */ |
| 489 | + if(fHide && isHidden){ |
| 490 | + fossil_warning("Skipping hidden checkin %s: %s.", zName, zUuid); |
| 491 | + continue; |
| 492 | + }else if(!fHide && !isHidden){ |
| 493 | + fossil_warning("Skipping non-hidden checkin %s: %s.", zName, zUuid); |
| 494 | + continue; |
| 495 | + } |
| 496 | + branch_cmd_tag_add(rid, fHide ? "*hidden" : "-hidden"); |
| 497 | + if(fVerbose!=0){ |
| 498 | + fossil_print("%s checkin [%s] %s\n", |
| 499 | + fHide ? "Hiding" : "Unhiding", |
| 500 | + zName, zUuid); |
| 501 | + } |
| 502 | + } |
| 503 | + branch_cmd_tag_finalize(fDryRun, fVerbose, zDateOvrd, zUserOvrd); |
| 504 | +} |
| 505 | + |
| 506 | +/* |
| 507 | +** Implementation of (branch close|reopen) subcommands. nStartAtArg is |
| 508 | +** the g.argv index to start reading branch/checkin names. The given |
| 509 | +** checkins are closed if fClose is true, else their "closed" tag (if |
| 510 | +** any) is cancelled. Fails fatally on error. |
| 511 | +*/ |
| 512 | +static void branch_cmd_close(int nStartAtArg, int fClose){ |
| 513 | + int argPos = nStartAtArg; /* g.argv pos with first branch name */ |
| 514 | + char * zUuid = 0; /* Resolved branch UUID. */ |
| 515 | + const int fVerbose = find_option("verbose","v",0)!=0; |
| 516 | + const int fDryRun = find_option("dry-run","n",0)!=0; |
| 517 | + const char *zDateOvrd = find_option("date-override",0,1); |
| 518 | + const char *zUserOvrd = find_option("user-override",0,1); |
| 519 | + |
| 520 | + verify_all_options(); |
| 521 | + db_begin_transaction(); |
| 522 | + for( ; argPos < g.argc; fossil_free(zUuid), ++argPos ){ |
| 523 | + const char * zName = g.argv[argPos]; |
| 524 | + const int rid = branch_resolve_name(zName, &zUuid); |
| 525 | + const int isClosed = leaf_is_closed(rid); |
| 526 | + if(!is_a_leaf(rid)){ |
| 527 | + /* This behaviour is different from /ci_edit closing, where |
| 528 | + ** is_a_leaf() adds a "+" tag and !is_a_leaf() adds a "*" |
| 529 | + ** tag. We might want to change this to match for consistency's |
| 530 | + ** sake, but it currently seems unnecessary to close/re-open a |
| 531 | + ** non-leaf. */ |
| 532 | + fossil_warning("Skipping non-leaf [%s] %s", zName, zUuid); |
| 533 | + continue; |
| 534 | + }else if(fClose && isClosed){ |
| 535 | + fossil_warning("Skipping closed leaf [%s] %s", zName, zUuid); |
| 536 | + continue; |
| 537 | + }else if(!fClose && !isClosed){ |
| 538 | + fossil_warning("Skipping non-closed leaf [%s] %s", zName, zUuid); |
| 539 | + continue; |
| 540 | + } |
| 541 | + branch_cmd_tag_add(rid, fClose ? "+closed" : "-closed"); |
| 542 | + if(fVerbose!=0){ |
| 543 | + fossil_print("%s branch [%s] %s\n", |
| 544 | + fClose ? "Closing" : "Re-opening", |
| 545 | + zName, zUuid); |
| 546 | + } |
| 547 | + } |
| 548 | + branch_cmd_tag_finalize(fDryRun, fVerbose, zDateOvrd, zUserOvrd); |
| 549 | +} |
| 349 | 550 | |
| 350 | 551 | /* |
| 351 | 552 | ** COMMAND: branch |
| 352 | 553 | ** |
| 353 | 554 | ** Usage: %fossil branch SUBCOMMAND ... ?OPTIONS? |
| 354 | 555 | ** |
| 355 | 556 | ** Run various subcommands to manage branches of the open repository or |
| 356 | 557 | ** of the repository identified by the -R or --repository option. |
| 357 | 558 | ** |
| 559 | +** > fossil branch close|reopen ?OPTIONS? BRANCH-NAME ?...BRANCH-NAMES? |
| 560 | +** |
| 561 | +** Adds or cancels the "closed" tag to one or more branches. |
| 562 | +** It accepts arbitrary unambiguous symbolic names but |
| 563 | +** will only resolve checkin names and skips any which resolve |
| 564 | +** to non-leaf checkins. Options: |
| 565 | +** -n|--dry-run do not commit changes and dump artifact |
| 566 | +** to stdout |
| 567 | +** -v|--verbose output more information |
| 568 | +** --date-override DATE DATE to use instead of 'now' |
| 569 | +** --user-override USER USER to use instead of the current default |
| 570 | +** |
| 358 | 571 | ** > fossil branch current |
| 359 | 572 | ** |
| 360 | 573 | ** Print the name of the branch for the current check-out |
| 574 | +** |
| 575 | +** > fossil branch hide|unhide ?OPTIONS? BRANCH-NAME ?...BRANCH-NAMES? |
| 576 | +** |
| 577 | +** Adds or cancels the "hidden" tag for the specified branches or |
| 578 | +** or checkin IDs. Accepts the same options as the close |
| 579 | +** subcommand. |
| 361 | 580 | ** |
| 362 | 581 | ** > fossil branch info BRANCH-NAME |
| 363 | 582 | ** |
| 364 | 583 | ** Print information about a branch |
| 365 | 584 | ** |
| | @@ -377,17 +596,17 @@ |
| 377 | 596 | ** |
| 378 | 597 | ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS? |
| 379 | 598 | ** |
| 380 | 599 | ** Create a new branch BRANCH-NAME off of check-in BASIS. |
| 381 | 600 | ** Supported options for this subcommand include: |
| 382 | | -** --private branch is private (i.e., remains local) |
| 383 | | -** --bgcolor COLOR use COLOR instead of automatic background |
| 601 | +** --private branch is private (i.e., remains local) |
| 602 | +** --bgcolor COLOR use COLOR instead of automatic background |
| 384 | 603 | ** ("auto" lets Fossil choose it automatically, |
| 385 | | -** even for private branches) |
| 386 | | -** --nosign do not sign contents on this branch |
| 387 | | -** --date-override DATE DATE to use instead of 'now' |
| 388 | | -** --user-override USER USER to use instead of the current default |
| 604 | +** even for private branches) |
| 605 | +** --nosign do not sign contents on this branch |
| 606 | +** --date-override DATE DATE to use instead of 'now' |
| 607 | +** --user-override USER USER to use instead of the current default |
| 389 | 608 | ** |
| 390 | 609 | ** DATE may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in |
| 391 | 610 | ** year-month-day form, it may be truncated, the "T" may be |
| 392 | 611 | ** replaced by a space, and it may also name a timezone offset |
| 393 | 612 | ** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward). |
| | @@ -452,13 +671,33 @@ |
| 452 | 671 | fossil_print("%s%s\n", (isCur ? "* " : " "), zBr); |
| 453 | 672 | } |
| 454 | 673 | db_finalize(&q); |
| 455 | 674 | }else if( strncmp(zCmd,"new",n)==0 ){ |
| 456 | 675 | branch_new(); |
| 676 | + }else if( strncmp(zCmd,"close",5)==0 ){ |
| 677 | + if(g.argc<4){ |
| 678 | + usage("branch close branch-name(s)..."); |
| 679 | + } |
| 680 | + branch_cmd_close(3, 1); |
| 681 | + }else if( strncmp(zCmd,"reopen",6)==0 ){ |
| 682 | + if(g.argc<4){ |
| 683 | + usage("branch reopen branch-name(s)..."); |
| 684 | + } |
| 685 | + branch_cmd_close(3, 0); |
| 686 | + }else if( strncmp(zCmd,"hide",4)==0 ){ |
| 687 | + if(g.argc<4){ |
| 688 | + usage("branch hide branch-name(s)..."); |
| 689 | + } |
| 690 | + branch_cmd_hide(3,1); |
| 691 | + }else if( strncmp(zCmd,"unhide",6)==0 ){ |
| 692 | + if(g.argc<4){ |
| 693 | + usage("branch unhide branch-name(s)..."); |
| 694 | + } |
| 695 | + branch_cmd_hide(3,0); |
| 457 | 696 | }else{ |
| 458 | 697 | fossil_fatal("branch subcommand should be one of: " |
| 459 | | - "current info list ls new"); |
| 698 | + "close current hide info list ls new reopen unhide"); |
| 460 | 699 | } |
| 461 | 700 | } |
| 462 | 701 | |
| 463 | 702 | /* |
| 464 | 703 | ** This is the new-style branch-list page that shows the branch names |
| 465 | 704 | |