| | @@ -27,18 +27,20 @@ |
| 27 | 27 | #if INTERFACE |
| 28 | 28 | /* |
| 29 | 29 | ** Configuration transfers occur in groups. These are the allowed |
| 30 | 30 | ** groupings: |
| 31 | 31 | */ |
| 32 | | -#define CONFIGSET_SKIN 0x000001 /* WWW interface appearance */ |
| 33 | | -#define CONFIGSET_TKT 0x000002 /* Ticket configuration */ |
| 34 | | -#define CONFIGSET_PROJ 0x000004 /* Project name */ |
| 35 | | -#define CONFIGSET_SHUN 0x000008 /* Shun settings */ |
| 36 | | -#define CONFIGSET_USER 0x000010 /* The USER table */ |
| 37 | | -#define CONFIGSET_ADDR 0x000020 /* The CONCEALED table */ |
| 38 | | - |
| 39 | | -#define CONFIGSET_ALL 0xffffff /* Everything */ |
| 32 | +#define CONFIGSET_SKIN 0x000001 /* WWW interface appearance */ |
| 33 | +#define CONFIGSET_TKT 0x000002 /* Ticket configuration */ |
| 34 | +#define CONFIGSET_PROJ 0x000004 /* Project name */ |
| 35 | +#define CONFIGSET_SHUN 0x000008 /* Shun settings */ |
| 36 | +#define CONFIGSET_USER 0x000010 /* The USER table */ |
| 37 | +#define CONFIGSET_ADDR 0x000020 /* The CONCEALED table */ |
| 38 | + |
| 39 | +#define CONFIGSET_ALL 0x0000ff /* Everything */ |
| 40 | + |
| 41 | +#define CONFIGSET_OVERWRITE 0x100000 /* Causes overwrite instead of merge */ |
| 40 | 42 | |
| 41 | 43 | #endif /* INTERFACE */ |
| 42 | 44 | |
| 43 | 45 | /* |
| 44 | 46 | ** Names of the configuration sets |
| | @@ -127,12 +129,17 @@ |
| 127 | 129 | ** login credentials and has sufficient capabilities to access the requested |
| 128 | 130 | ** information. |
| 129 | 131 | */ |
| 130 | 132 | int configure_is_exportable(const char *zName){ |
| 131 | 133 | int i; |
| 134 | + int n = strlen(zName); |
| 135 | + if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){ |
| 136 | + zName++; |
| 137 | + n -= 2; |
| 138 | + } |
| 132 | 139 | for(i=0; i<count(aConfig); i++){ |
| 133 | | - if( fossil_strcmp(zName, aConfig[i].zName)==0 ){ |
| 140 | + if( memcmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){ |
| 134 | 141 | int m = aConfig[i].groupMask; |
| 135 | 142 | if( !g.okAdmin ){ |
| 136 | 143 | m &= ~CONFIGSET_USER; |
| 137 | 144 | } |
| 138 | 145 | if( !g.okRdAddr ){ |
| | @@ -200,38 +207,39 @@ |
| 200 | 207 | } |
| 201 | 208 | |
| 202 | 209 | /* |
| 203 | 210 | ** Two SQL functions: |
| 204 | 211 | ** |
| 205 | | -** flag_test(int) |
| 206 | | -** flag_clear(int) |
| 212 | +** config_is_reset(int) |
| 213 | +** config_reset(int) |
| 207 | 214 | ** |
| 208 | | -** The flag_test() function takes the integer valued argument and |
| 209 | | -** ANDs it against the static variable "flag_value" below. The |
| 210 | | -** function returns TRUE or false depending on the result. The |
| 211 | | -** flag_clear() function masks off the bits from "flag_value" that |
| 215 | +** The config_is_reset() function takes the integer valued argument and |
| 216 | +** ANDs it against the static variable "configHasBeenReset" below. The |
| 217 | +** function returns TRUE or FALSE depending on the result depending on |
| 218 | +** whether or not the corresponding configuration table has been reset. The |
| 219 | +** config_reset() function adds the bits to "configHasBeenReset" that |
| 212 | 220 | ** are given in the argument. |
| 213 | 221 | ** |
| 214 | 222 | ** These functions are used below in the WHEN clause of a trigger to |
| 215 | 223 | ** get the trigger to fire exactly once. |
| 216 | 224 | */ |
| 217 | | -static int flag_value = 0xffff; |
| 218 | | -static void flag_test_function( |
| 225 | +static int configHasBeenReset = 0; |
| 226 | +static void config_is_reset_function( |
| 219 | 227 | sqlite3_context *context, |
| 220 | 228 | int argc, |
| 221 | 229 | sqlite3_value **argv |
| 222 | 230 | ){ |
| 223 | 231 | int m = sqlite3_value_int(argv[0]); |
| 224 | | - sqlite3_result_int(context, (flag_value&m)!=0 ); |
| 232 | + sqlite3_result_int(context, (configHasBeenReset&m)!=0 ); |
| 225 | 233 | } |
| 226 | | -static void flag_clear_function( |
| 234 | +static void config_reset_function( |
| 227 | 235 | sqlite3_context *context, |
| 228 | 236 | int argc, |
| 229 | 237 | sqlite3_value **argv |
| 230 | 238 | ){ |
| 231 | 239 | int m = sqlite3_value_int(argv[0]); |
| 232 | | - flag_value &= ~m; |
| 240 | + configHasBeenReset |= m; |
| 233 | 241 | } |
| 234 | 242 | |
| 235 | 243 | /* |
| 236 | 244 | ** Create the temporary _xfer_reportfmt and _xfer_user tables that are |
| 237 | 245 | ** necessary in order to evalute the SQL text generated by the |
| | @@ -273,30 +281,30 @@ |
| 273 | 281 | ** existing data. |
| 274 | 282 | */ |
| 275 | 283 | if( replaceFlag ){ |
| 276 | 284 | static const char zSQL2[] = |
| 277 | 285 | @ CREATE TRIGGER _xfer_r1 BEFORE INSERT ON _xfer_reportfmt |
| 278 | | - @ WHEN flag_test(1) BEGIN |
| 286 | + @ WHEN NOT config_is_reset(2) BEGIN |
| 279 | 287 | @ DELETE FROM _xfer_reportfmt; |
| 280 | | - @ SELECT flag_clear(1); |
| 288 | + @ SELECT config_reset(2); |
| 281 | 289 | @ END; |
| 282 | 290 | @ CREATE TRIGGER _xfer_r2 BEFORE INSERT ON _xfer_user |
| 283 | | - @ WHEN flag_test(2) BEGIN |
| 291 | + @ WHEN NOT config_is_reset(16) BEGIN |
| 284 | 292 | @ DELETE FROM _xfer_user; |
| 285 | | - @ SELECT flag_clear(2); |
| 293 | + @ SELECT config_reset(16); |
| 286 | 294 | @ END; |
| 287 | 295 | @ CREATE TEMP TRIGGER _xfer_r3 BEFORE INSERT ON shun |
| 288 | | - @ WHEN flag_test(4) BEGIN |
| 296 | + @ WHEN NOT config_is_reset(8) BEGIN |
| 289 | 297 | @ DELETE FROM shun; |
| 290 | | - @ SELECT flag_clear(4); |
| 298 | + @ SELECT config_reset(8); |
| 291 | 299 | @ END; |
| 292 | 300 | ; |
| 293 | | - sqlite3_create_function(g.db, "flag_test", 1, SQLITE_UTF8, 0, |
| 294 | | - flag_test_function, 0, 0); |
| 295 | | - sqlite3_create_function(g.db, "flag_clear", 1, SQLITE_UTF8, 0, |
| 296 | | - flag_clear_function, 0, 0); |
| 297 | | - flag_value = 0xffff; |
| 301 | + sqlite3_create_function(g.db, "config_is_reset", 1, SQLITE_UTF8, 0, |
| 302 | + config_is_reset_function, 0, 0); |
| 303 | + sqlite3_create_function(g.db, "config_reset", 1, SQLITE_UTF8, 0, |
| 304 | + config_reset_function, 0, 0); |
| 305 | + configHasBeenReset = 0; |
| 298 | 306 | db_multi_exec(zSQL2); |
| 299 | 307 | } |
| 300 | 308 | } |
| 301 | 309 | |
| 302 | 310 | /* |
| | @@ -384,16 +392,17 @@ |
| 384 | 392 | ** table like _fer_reportfmt or _xfer_user. Such tables must be created |
| 385 | 393 | ** ahead of time using configure_prepare_to_receive(). Then after multiple |
| 386 | 394 | ** calls to this routine, configure_finalize_receive() to transfer the |
| 387 | 395 | ** information received into the true target table. |
| 388 | 396 | */ |
| 389 | | -void configure_receive(const char *zName, Blob *pContent, int mask){ |
| 397 | +void configure_receive(const char *zName, Blob *pContent, int groupMask){ |
| 390 | 398 | if( zName[0]=='/' ){ |
| 391 | 399 | /* The new format */ |
| 392 | 400 | char *azToken[12]; |
| 393 | 401 | int nToken = 0; |
| 394 | 402 | int ii, jj; |
| 403 | + int thisMask; |
| 395 | 404 | Blob name, value, sql; |
| 396 | 405 | static const struct receiveType { |
| 397 | 406 | const char *zName; |
| 398 | 407 | const char *zPrimKey; |
| 399 | 408 | int nField; |
| | @@ -425,29 +434,38 @@ |
| 425 | 434 | if( !safeSql(z) ) return; |
| 426 | 435 | if( nToken>=count(azToken) ) break; |
| 427 | 436 | } |
| 428 | 437 | if( nToken<2 ) return; |
| 429 | 438 | if( aType[ii].zName[0]=='/' ){ |
| 430 | | - if( (configure_is_exportable(azToken[1]) & mask)==0 ) return; |
| 431 | | - }else{ |
| 432 | | - if( (configure_is_exportable(aType[ii].zName) & mask)==0 ) return; |
| 433 | | - } |
| 434 | | - |
| 435 | | - blob_reset(&sql); |
| 436 | | - blob_appendf(&sql, "INSERT OR IGNORE INTO %s(%s, mtime", |
| 437 | | - &zName[1], aType[ii].zPrimKey); |
| 439 | + thisMask = configure_is_exportable(azToken[1]); |
| 440 | + }else{ |
| 441 | + thisMask = configure_is_exportable(aType[ii].zName); |
| 442 | + } |
| 443 | + if( (thisMask & groupMask)==0 ) return; |
| 444 | + |
| 445 | + blob_zero(&sql); |
| 446 | + if( groupMask & CONFIGSET_OVERWRITE ){ |
| 447 | + if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){ |
| 448 | + db_multi_exec("DELETE FROM %s", &aType[ii].zName[1]); |
| 449 | + configHasBeenReset |= thisMask; |
| 450 | + } |
| 451 | + blob_append(&sql, "REPLACE INTO ", -1); |
| 452 | + }else{ |
| 453 | + blob_append(&sql, "INSERT OR IGNORE INTO ", -1); |
| 454 | + } |
| 455 | + blob_appendf(&sql, "%s(%s, mtime", &zName[1], aType[ii].zPrimKey); |
| 438 | 456 | for(jj=2; jj<nToken; jj+=2){ |
| 439 | 457 | blob_appendf(&sql, ",%s", azToken[jj]); |
| 440 | 458 | } |
| 441 | 459 | blob_appendf(&sql,") VALUES(%s,%s", azToken[1], azToken[0]); |
| 442 | 460 | for(jj=2; jj<nToken; jj+=2){ |
| 443 | 461 | blob_appendf(&sql, ",%s", azToken[jj+1]); |
| 444 | 462 | } |
| 445 | | - db_multi_exec("%s", blob_str(&sql)); |
| 463 | + db_multi_exec("%s)", blob_str(&sql)); |
| 446 | 464 | if( db_changes()==0 ){ |
| 447 | 465 | blob_reset(&sql); |
| 448 | | - blob_appendf(&sql, "UPDATE %s SET mtime=%s,", &zName[1], azToken[0]); |
| 466 | + blob_appendf(&sql, "UPDATE %s SET mtime=%s", &zName[1], azToken[0]); |
| 449 | 467 | for(jj=2; jj<nToken; jj+=2){ |
| 450 | 468 | blob_appendf(&sql, ", %s=%s", azToken[jj], azToken[jj+1]); |
| 451 | 469 | } |
| 452 | 470 | blob_appendf(&sql, " WHERE %s=%s AND mtime<%s", |
| 453 | 471 | aType[ii].zPrimKey, azToken[1], azToken[0]); |
| | @@ -454,15 +472,15 @@ |
| 454 | 472 | db_multi_exec("%s", blob_str(&sql)); |
| 455 | 473 | } |
| 456 | 474 | blob_reset(&sql); |
| 457 | 475 | }else{ |
| 458 | 476 | /* Otherwise, the old format */ |
| 459 | | - if( (configure_is_exportable(zName) & mask)==0 ) return; |
| 477 | + if( (configure_is_exportable(zName) & groupMask)==0 ) return; |
| 460 | 478 | if( strcmp(zName, "logo-image")==0 ){ |
| 461 | 479 | Stmt ins; |
| 462 | 480 | db_prepare(&ins, |
| 463 | | - "REPLACE INTO config(name, value) VALUES(:name, :value)" |
| 481 | + "REPLACE INTO config(name, value, mtime) VALUES(:name, :value, now())" |
| 464 | 482 | ); |
| 465 | 483 | db_bind_text(&ins, ":name", zName); |
| 466 | 484 | db_bind_blob(&ins, ":value", pContent); |
| 467 | 485 | db_step(&ins); |
| 468 | 486 | db_finalize(&ins); |
| | @@ -473,17 +491,47 @@ |
| 473 | 491 | ** point. |
| 474 | 492 | */ |
| 475 | 493 | db_multi_exec("%s", blob_str(pContent)); |
| 476 | 494 | }else{ |
| 477 | 495 | db_multi_exec( |
| 478 | | - "REPLACE INTO config(name,value) VALUES(%Q,%Q)", |
| 496 | + "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())", |
| 479 | 497 | zName, blob_str(pContent) |
| 480 | 498 | ); |
| 481 | 499 | } |
| 482 | 500 | } |
| 483 | 501 | } |
| 484 | 502 | |
| 503 | +/* |
| 504 | +** Process a file full of "config" cards. |
| 505 | +*/ |
| 506 | +void configure_receive_all(Blob *pIn, int groupMask){ |
| 507 | + Blob line; |
| 508 | + int nToken; |
| 509 | + int size; |
| 510 | + Blob aToken[4]; |
| 511 | + |
| 512 | + configHasBeenReset = 0; |
| 513 | + while( blob_line(pIn, &line) ){ |
| 514 | + if( blob_buffer(&line)[0]=='#' ) continue; |
| 515 | + nToken = blob_tokenize(&line, aToken, count(aToken)); |
| 516 | + if( blob_eq(&aToken[0],"config") |
| 517 | + && nToken==3 |
| 518 | + && blob_is_int(&aToken[2], &size) |
| 519 | + ){ |
| 520 | + const char *zName = blob_str(&aToken[1]); |
| 521 | + Blob content; |
| 522 | + blob_zero(&content); |
| 523 | + blob_extract(pIn, size, &content); |
| 524 | + g.okAdmin = g.okRdAddr = 1; |
| 525 | + configure_receive(zName, &content, groupMask); |
| 526 | + blob_reset(&content); |
| 527 | + blob_seek(pIn, 1, BLOB_SEEK_CUR); |
| 528 | + } |
| 529 | + } |
| 530 | +} |
| 531 | + |
| 532 | + |
| 485 | 533 | /* |
| 486 | 534 | ** Send "config" cards using the new format for all elements of a group |
| 487 | 535 | ** that have recently changed. |
| 488 | 536 | ** |
| 489 | 537 | ** Output goes into pOut. The groupMask identifies the group(s) to be sent. |
| | @@ -648,40 +696,25 @@ |
| 648 | 696 | /* |
| 649 | 697 | ** Write SQL text into file zFilename that will restore the configuration |
| 650 | 698 | ** area identified by mask to its current state from any other state. |
| 651 | 699 | */ |
| 652 | 700 | static void export_config( |
| 653 | | - int mask, /* Mask indicating which configuration to export */ |
| 701 | + int groupMask, /* Mask indicating which configuration to export */ |
| 654 | 702 | const char *zMask, /* Name of the configuration */ |
| 703 | + sqlite3_int64 iStart, /* Start date */ |
| 655 | 704 | const char *zFilename /* Write into this file */ |
| 656 | 705 | ){ |
| 657 | | - int i; |
| 658 | 706 | Blob out; |
| 659 | 707 | blob_zero(&out); |
| 660 | 708 | blob_appendf(&out, |
| 661 | | - "-- The \"%s\" configuration exported from\n" |
| 662 | | - "-- repository \"%s\"\n" |
| 663 | | - "-- on %s\n", |
| 709 | + "# The \"%s\" configuration exported from\n" |
| 710 | + "# repository \"%s\"\n" |
| 711 | + "# on %s\n", |
| 664 | 712 | zMask, g.zRepositoryName, |
| 665 | 713 | db_text(0, "SELECT datetime('now')") |
| 666 | 714 | ); |
| 667 | | - for(i=0; i<count(aConfig); i++){ |
| 668 | | - if( (aConfig[i].groupMask & mask)!=0 ){ |
| 669 | | - const char *zName = aConfig[i].zName; |
| 670 | | - if( zName[0]!='@' ){ |
| 671 | | - char *zValue = db_text(0, |
| 672 | | - "SELECT quote(value) FROM config WHERE name=%Q", zName); |
| 673 | | - if( zValue ){ |
| 674 | | - blob_appendf(&out,"REPLACE INTO config VALUES(%Q,%s);\n", |
| 675 | | - zName, zValue); |
| 676 | | - } |
| 677 | | - free(zValue); |
| 678 | | - }else{ |
| 679 | | - configure_render_special_name(zName, &out); |
| 680 | | - } |
| 681 | | - } |
| 682 | | - } |
| 715 | + configure_send_group(&out, groupMask, iStart); |
| 683 | 716 | blob_write_to_file(&out, zFilename); |
| 684 | 717 | blob_reset(&out); |
| 685 | 718 | } |
| 686 | 719 | |
| 687 | 720 | |
| | @@ -738,25 +771,39 @@ |
| 738 | 771 | db_find_and_open_repository(0, 0); |
| 739 | 772 | zMethod = g.argv[2]; |
| 740 | 773 | n = strlen(zMethod); |
| 741 | 774 | if( strncmp(zMethod, "export", n)==0 ){ |
| 742 | 775 | int mask; |
| 776 | + const char *zSince = find_option("since",0,1); |
| 777 | + sqlite3_int64 iStart; |
| 743 | 778 | if( g.argc!=5 ){ |
| 744 | 779 | usage("export AREA FILENAME"); |
| 745 | 780 | } |
| 746 | 781 | mask = find_area(g.argv[3]); |
| 747 | | - export_config(mask, g.argv[3], g.argv[4]); |
| 782 | + if( zSince ){ |
| 783 | + iStart = db_multi_exec( |
| 784 | + "SELECT coalesce(strftime('%%s',%Q),strftime('%%s','now',%Q))+0", |
| 785 | + zSince, zSince |
| 786 | + ); |
| 787 | + }else{ |
| 788 | + iStart = 0; |
| 789 | + } |
| 790 | + export_config(mask, g.argv[3], iStart, g.argv[4]); |
| 748 | 791 | }else |
| 749 | 792 | if( strncmp(zMethod, "import", n)==0 |
| 750 | 793 | || strncmp(zMethod, "merge", n)==0 ){ |
| 751 | 794 | Blob in; |
| 795 | + int groupMask; |
| 752 | 796 | if( g.argc!=4 ) usage(mprintf("%s FILENAME",zMethod)); |
| 753 | 797 | blob_read_from_file(&in, g.argv[3]); |
| 754 | 798 | db_begin_transaction(); |
| 755 | | - configure_prepare_to_receive(zMethod[0]=='i'); |
| 756 | | - db_multi_exec("%s", blob_str(&in)); |
| 757 | | - configure_finalize_receive(); |
| 799 | + if( zMethod[0]=='i' ){ |
| 800 | + groupMask = CONFIGSET_ALL | CONFIGSET_OVERWRITE; |
| 801 | + }else{ |
| 802 | + groupMask = CONFIGSET_ALL; |
| 803 | + } |
| 804 | + configure_receive_all(&in, groupMask); |
| 758 | 805 | db_end_transaction(0); |
| 759 | 806 | }else |
| 760 | 807 | if( strncmp(zMethod, "pull", n)==0 || strncmp(zMethod, "push", n)==0 ){ |
| 761 | 808 | int mask; |
| 762 | 809 | const char *zServer; |
| | @@ -793,11 +840,11 @@ |
| 793 | 840 | if( g.argc!=4 ) usage("reset AREA"); |
| 794 | 841 | mask = find_area(g.argv[3]); |
| 795 | 842 | zBackup = db_text(0, |
| 796 | 843 | "SELECT strftime('config-backup-%%Y%%m%%d%%H%%M%%f','now')"); |
| 797 | 844 | db_begin_transaction(); |
| 798 | | - export_config(mask, g.argv[3], zBackup); |
| 845 | + export_config(mask, g.argv[3], 0, zBackup); |
| 799 | 846 | for(i=0; i<count(aConfig); i++){ |
| 800 | 847 | const char *zName = aConfig[i].zName; |
| 801 | 848 | if( (aConfig[i].groupMask & mask)==0 ) continue; |
| 802 | 849 | if( zName[0]!='@' ){ |
| 803 | 850 | db_multi_exec("DELETE FROM config WHERE name=%Q", zName); |
| 804 | 851 | |