Fossil SCM
Code in place for "patch pull" and "patch push".
Commit
ccfdc89c962ddc7bdad883c4553b57a58e13a1194e897494382823b149c77e86
Parent
9180106327c3863…
2 files changed
+10
-3
+178
-88
+10
-3
| --- src/http_transport.c | ||
| +++ src/http_transport.c | ||
| @@ -97,25 +97,32 @@ | ||
| 97 | 97 | #ifdef _WIN32 |
| 98 | 98 | static const char zDefaultSshCmd[] = "plink -ssh -T"; |
| 99 | 99 | #else |
| 100 | 100 | static const char zDefaultSshCmd[] = "ssh -e none -T"; |
| 101 | 101 | #endif |
| 102 | + | |
| 103 | +/* | |
| 104 | +** Initialize a Blob to the name of the configured SSH command. | |
| 105 | +*/ | |
| 106 | +void transport_ssh_command(Blob *p){ | |
| 107 | + char *zSsh; /* The base SSH command */ | |
| 108 | + zSsh = db_get("ssh-command", zDefaultSshCmd); | |
| 109 | + blob_init(p, zSsh, -1); | |
| 110 | +} | |
| 102 | 111 | |
| 103 | 112 | /* |
| 104 | 113 | ** SSH initialization of the transport layer |
| 105 | 114 | */ |
| 106 | 115 | int transport_ssh_open(UrlData *pUrlData){ |
| 107 | 116 | /* For SSH we need to create and run SSH fossil http |
| 108 | 117 | ** to talk to the remote machine. |
| 109 | 118 | */ |
| 110 | - char *zSsh; /* The base SSH command */ | |
| 111 | 119 | Blob zCmd; /* The SSH command */ |
| 112 | 120 | char *zHost; /* The host name to contact */ |
| 113 | 121 | |
| 114 | 122 | socket_ssh_resolve_addr(pUrlData); |
| 115 | - zSsh = db_get("ssh-command", zDefaultSshCmd); | |
| 116 | - blob_init(&zCmd, zSsh, -1); | |
| 123 | + transport_ssh_command(&zCmd); | |
| 117 | 124 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| 118 | 125 | #ifdef _WIN32 |
| 119 | 126 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 120 | 127 | #else |
| 121 | 128 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 122 | 129 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -97,25 +97,32 @@ | |
| 97 | #ifdef _WIN32 |
| 98 | static const char zDefaultSshCmd[] = "plink -ssh -T"; |
| 99 | #else |
| 100 | static const char zDefaultSshCmd[] = "ssh -e none -T"; |
| 101 | #endif |
| 102 | |
| 103 | /* |
| 104 | ** SSH initialization of the transport layer |
| 105 | */ |
| 106 | int transport_ssh_open(UrlData *pUrlData){ |
| 107 | /* For SSH we need to create and run SSH fossil http |
| 108 | ** to talk to the remote machine. |
| 109 | */ |
| 110 | char *zSsh; /* The base SSH command */ |
| 111 | Blob zCmd; /* The SSH command */ |
| 112 | char *zHost; /* The host name to contact */ |
| 113 | |
| 114 | socket_ssh_resolve_addr(pUrlData); |
| 115 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 116 | blob_init(&zCmd, zSsh, -1); |
| 117 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| 118 | #ifdef _WIN32 |
| 119 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 120 | #else |
| 121 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 122 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -97,25 +97,32 @@ | |
| 97 | #ifdef _WIN32 |
| 98 | static const char zDefaultSshCmd[] = "plink -ssh -T"; |
| 99 | #else |
| 100 | static const char zDefaultSshCmd[] = "ssh -e none -T"; |
| 101 | #endif |
| 102 | |
| 103 | /* |
| 104 | ** Initialize a Blob to the name of the configured SSH command. |
| 105 | */ |
| 106 | void transport_ssh_command(Blob *p){ |
| 107 | char *zSsh; /* The base SSH command */ |
| 108 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 109 | blob_init(p, zSsh, -1); |
| 110 | } |
| 111 | |
| 112 | /* |
| 113 | ** SSH initialization of the transport layer |
| 114 | */ |
| 115 | int transport_ssh_open(UrlData *pUrlData){ |
| 116 | /* For SSH we need to create and run SSH fossil http |
| 117 | ** to talk to the remote machine. |
| 118 | */ |
| 119 | Blob zCmd; /* The SSH command */ |
| 120 | char *zHost; /* The host name to contact */ |
| 121 | |
| 122 | socket_ssh_resolve_addr(pUrlData); |
| 123 | transport_ssh_command(&zCmd); |
| 124 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| 125 | #ifdef _WIN32 |
| 126 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 127 | #else |
| 128 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 129 |
+178
-88
| --- src/patch.c | ||
| +++ src/patch.c | ||
| @@ -19,16 +19,26 @@ | ||
| 19 | 19 | */ |
| 20 | 20 | #include "config.h" |
| 21 | 21 | #include "patch.h" |
| 22 | 22 | #include <assert.h> |
| 23 | 23 | |
| 24 | +/* | |
| 25 | +** Additional windows configuration for popen */ | |
| 26 | +#if defined(_WIN32) | |
| 27 | +# include <io.h> | |
| 28 | +# include <fcntl.h> | |
| 29 | +# undef popen | |
| 30 | +# define popen _popen | |
| 31 | +#endif | |
| 32 | + | |
| 24 | 33 | /* |
| 25 | 34 | ** Flags passed from the main patch_cmd() routine into subfunctions used |
| 26 | 35 | ** to implement the various subcommands. |
| 27 | 36 | */ |
| 28 | 37 | #define PATCH_DRYRUN 0x0001 |
| 29 | 38 | #define PATCH_VERBOSE 0x0002 |
| 39 | +#define PATCH_FORCE 0x0004 | |
| 30 | 40 | |
| 31 | 41 | /* |
| 32 | 42 | ** Implementation of the "readfile(X)" SQL function. The entire content |
| 33 | 43 | ** of the checkout file named X is read and returned as a BLOB. |
| 34 | 44 | */ |
| @@ -113,11 +123,11 @@ | ||
| 113 | 123 | |
| 114 | 124 | /* |
| 115 | 125 | ** Generate a binary patch file and store it into the file |
| 116 | 126 | ** named zOut. |
| 117 | 127 | */ |
| 118 | -void patch_create(const char *zOut){ | |
| 128 | +void patch_create(const char *zOut, FILE *out){ | |
| 119 | 129 | int vid; |
| 120 | 130 | |
| 121 | 131 | if( zOut && file_isdir(zOut, ExtFILE)!=0 ){ |
| 122 | 132 | fossil_fatal("patch file already exists: %s", zOut); |
| 123 | 133 | } |
| @@ -147,13 +157,10 @@ | ||
| 147 | 157 | vid = db_lget_int("checkout", 0); |
| 148 | 158 | vfile_check_signature(vid, CKSIG_ENOTFILE); |
| 149 | 159 | db_multi_exec( |
| 150 | 160 | "INSERT INTO patch.cfg(key,value)" |
| 151 | 161 | "SELECT 'baseline',uuid FROM blob WHERE rid=%d", vid); |
| 152 | - if( db_exists("SELECT 1 FROM vmerge") ){ | |
| 153 | - db_multi_exec("INSERT INTO patch.cfg(key,value)VALUES('merged',1);"); | |
| 154 | - } | |
| 155 | 162 | |
| 156 | 163 | /* New files */ |
| 157 | 164 | db_multi_exec( |
| 158 | 165 | "INSERT INTO patch.chng(pathname,hash,isexe,islink,delta)" |
| 159 | 166 | " SELECT pathname, NULL, isexe, islink," |
| @@ -198,53 +205,38 @@ | ||
| 198 | 205 | unsigned char *pData; |
| 199 | 206 | pData = sqlite3_serialize(g.db, "patch", &sz, 0); |
| 200 | 207 | if( pData==0 ){ |
| 201 | 208 | fossil_fatal("out of memory"); |
| 202 | 209 | } |
| 203 | - fossil_print("%025lld\n", sz); | |
| 204 | - fwrite(pData, sz, 1, stdout); | |
| 210 | + fwrite(pData, sz, 1, out); | |
| 205 | 211 | sqlite3_free(pData); |
| 206 | - fflush(stdout); | |
| 212 | + fflush(out); | |
| 207 | 213 | } |
| 208 | 214 | } |
| 209 | 215 | |
| 210 | 216 | /* |
| 211 | 217 | ** Attempt to load and validate a patchfile identified by the first |
| 212 | 218 | ** argument. |
| 213 | 219 | */ |
| 214 | -void patch_attach(const char *zIn){ | |
| 220 | +void patch_attach(const char *zIn, FILE *in){ | |
| 215 | 221 | Stmt q; |
| 216 | 222 | if( g.db==0 ){ |
| 217 | 223 | sqlite3_open(":memory:", &g.db); |
| 218 | 224 | } |
| 219 | 225 | if( zIn==0 ){ |
| 220 | - char zBuf[26]; | |
| 221 | - size_t n; | |
| 222 | - sqlite3_int64 sz; | |
| 223 | - unsigned char *aIn; | |
| 224 | - | |
| 225 | - n = fread(zBuf, sizeof(zBuf), 1, stdin); | |
| 226 | - if( n!=1 ){ | |
| 227 | - fossil_fatal("unable to read size of input\n"); | |
| 228 | - } | |
| 229 | - zBuf[sizeof(zBuf)-1] = 0; | |
| 230 | - sz = atoll(zBuf); | |
| 231 | - if( sz<512 || (sz%512)!=0 ){ | |
| 232 | - fossil_fatal("bad size for input: %lld", sz); | |
| 233 | - } | |
| 234 | - aIn = sqlite3_malloc64( sz ); | |
| 235 | - if( aIn==0 ){ | |
| 236 | - fossil_fatal("out of memory"); | |
| 237 | - } | |
| 238 | - n = fread(aIn, 1, sz, stdin); | |
| 239 | - if( n!=sz ){ | |
| 240 | - fossil_fatal("got only %lld of %lld input bytes", | |
| 241 | - (sqlite3_int64)n, sz); | |
| 242 | - } | |
| 226 | + Blob buf; | |
| 227 | + int rc; | |
| 228 | + int sz; | |
| 229 | + const unsigned char *pData; | |
| 230 | + blob_init(&buf, 0, 0); | |
| 231 | + sz = blob_read_from_channel(&buf, in, -1); | |
| 232 | + pData = (const unsigned char*)blob_buffer(&buf); | |
| 243 | 233 | db_multi_exec("ATTACH ':memory:' AS patch"); |
| 244 | - sqlite3_deserialize(g.db, "patch", aIn, sz, sz, | |
| 245 | - SQLITE_DESERIALIZE_FREEONCLOSE); | |
| 234 | + rc = sqlite3_deserialize(g.db, "patch", pData, sz, sz, 0); | |
| 235 | + if( rc ){ | |
| 236 | + fossil_fatal("cannot open patch database: %s", sqlite3_errmsg(g.db)); | |
| 237 | + } | |
| 246 | 238 | }else if( !file_isfile(zIn, ExtFILE) ){ |
| 247 | 239 | fossil_fatal("no such file: %s", zIn); |
| 248 | 240 | }else{ |
| 249 | 241 | db_multi_exec("ATTACH %Q AS patch", zIn); |
| 250 | 242 | } |
| @@ -310,10 +302,14 @@ | ||
| 310 | 302 | ** and update all files. |
| 311 | 303 | */ |
| 312 | 304 | void patch_apply(unsigned mFlags){ |
| 313 | 305 | Stmt q; |
| 314 | 306 | Blob cmd; |
| 307 | + | |
| 308 | + if( (mFlags & PATCH_FORCE)==0 && unsaved_changes(0) ){ | |
| 309 | + fossil_fatal("there are unsaved changes in the current checkout"); | |
| 310 | + } | |
| 315 | 311 | blob_init(&cmd, 0, 0); |
| 316 | 312 | file_chdir(g.zLocalRoot, 0); |
| 317 | 313 | db_prepare(&q, |
| 318 | 314 | "SELECT patch.cfg.value" |
| 319 | 315 | " FROM patch.cfg, localdb.vvar" |
| @@ -503,10 +499,107 @@ | ||
| 503 | 499 | } |
| 504 | 500 | blob_reset(&cmd); |
| 505 | 501 | } |
| 506 | 502 | } |
| 507 | 503 | |
| 504 | +/* | |
| 505 | +** Find the filename of the patch file to be used by | |
| 506 | +** "fossil patch apply" or "fossil patch create". | |
| 507 | +** | |
| 508 | +** If the name is "-" return NULL. | |
| 509 | +** | |
| 510 | +** Otherwise, if there is a prior DIRECTORY argument, or if | |
| 511 | +** the --dir64 option is present, first chdir to the specified | |
| 512 | +** directory, and translate the name in the argument accordingly. | |
| 513 | +** | |
| 514 | +** | |
| 515 | +** The returned name is obtained from fossil_malloc() and should | |
| 516 | +** be freed by the caller. | |
| 517 | +*/ | |
| 518 | +static char *patch_find_patch_filename(const char *zCmdName){ | |
| 519 | + const char *zDir64 = find_option("dir64",0,1); | |
| 520 | + const char *zDir = 0; | |
| 521 | + const char *zBaseName; | |
| 522 | + char *zToFree = 0; | |
| 523 | + char *zPatchFile = 0; | |
| 524 | + if( zDir64 ){ | |
| 525 | + zToFree = decode64(zDir64, 0); | |
| 526 | + zDir = zToFree; | |
| 527 | + } | |
| 528 | + verify_all_options(); | |
| 529 | + if( g.argc!=4 && g.argc!=5 ){ | |
| 530 | + usage(mprintf("%s [DIRECTORY] FILENAME", zCmdName)); | |
| 531 | + } | |
| 532 | + if( g.argc==5 ){ | |
| 533 | + zDir = g.argv[3]; | |
| 534 | + zBaseName = g.argv[4]; | |
| 535 | + }else{ | |
| 536 | + zBaseName = g.argv[3]; | |
| 537 | + } | |
| 538 | + if( fossil_strcmp(zBaseName, "-")==0 ){ | |
| 539 | + zPatchFile = 0; | |
| 540 | + }else if( zDir ){ | |
| 541 | + zPatchFile = file_canonical_name_dup(g.argv[4]); | |
| 542 | + }else{ | |
| 543 | + zPatchFile = fossil_strdup(g.argv[3]); | |
| 544 | + } | |
| 545 | + if( zDir && file_chdir(zDir,0) ){ | |
| 546 | + fossil_fatal("cannot change to directory \"%s\"", zDir); | |
| 547 | + } | |
| 548 | + fossil_free(zToFree); | |
| 549 | + return zPatchFile; | |
| 550 | +} | |
| 551 | + | |
| 552 | +/* | |
| 553 | +** Create a FILE* that will execute the remote side of a push or pull | |
| 554 | +** using ssh (probably) or fossil for local pushes and pulls. Return | |
| 555 | +*/ | |
| 556 | +static FILE *patch_remote_command( | |
| 557 | + unsigned mFlags, /* flags */ | |
| 558 | + const char *zThisCmd, /* "push" or "pull" */ | |
| 559 | + const char *zRemoteCmd, /* "apply" or "create" */ | |
| 560 | + const char *zRW /* "w" or "r" */ | |
| 561 | +){ | |
| 562 | + char *zRemote; | |
| 563 | + char *zDir; | |
| 564 | + Blob cmd; | |
| 565 | + FILE *f; | |
| 566 | + const char *zForce = (mFlags & PATCH_FORCE)!=0 ? " -f" : ""; | |
| 567 | + if( g.argc!=4 ){ | |
| 568 | + usage(mprintf("%s [USER@]HOST:DIRECTORY", zThisCmd)); | |
| 569 | + } | |
| 570 | + zRemote = fossil_strdup(g.argv[3]); | |
| 571 | + zDir = strchr(zRemote,':'); | |
| 572 | + if( zDir==0 ){ | |
| 573 | + zDir = zRemote; | |
| 574 | + blob_init(&cmd, 0, 0); | |
| 575 | + blob_append_escaped_arg(&cmd, g.nameOfExe); | |
| 576 | + blob_appendf(&cmd, " patch %s%s %$ -", zRemoteCmd, zForce, zDir); | |
| 577 | + }else{ | |
| 578 | + Blob remote; | |
| 579 | + zDir[0] = 0; | |
| 580 | + zDir++; | |
| 581 | + transport_ssh_command(&cmd); | |
| 582 | + blob_append_escaped_arg(&cmd, zRemote); | |
| 583 | + blob_init(&remote, 0, 0); | |
| 584 | + blob_appendf(&remote, "fossil patch %s%s --dir64 %z -", | |
| 585 | + zRemoteCmd, zForce, encode64(zDir, -1)); | |
| 586 | + blob_append_escaped_arg(&cmd, blob_str(&remote)); | |
| 587 | + blob_reset(&remote); | |
| 588 | + } | |
| 589 | + if( mFlags & PATCH_VERBOSE ){ | |
| 590 | + fossil_print("# %s\n", blob_str(&cmd)); | |
| 591 | + fflush(stdout); | |
| 592 | + } | |
| 593 | + f = popen(blob_str(&cmd), zRW); | |
| 594 | + if( f==0 ){ | |
| 595 | + fossil_fatal("cannot run command: %s", blob_str(&cmd)); | |
| 596 | + } | |
| 597 | + blob_reset(&cmd); | |
| 598 | + return f; | |
| 599 | +} | |
| 600 | + | |
| 508 | 601 | |
| 509 | 602 | /* |
| 510 | 603 | ** COMMAND: patch |
| 511 | 604 | ** |
| 512 | 605 | ** Usage: %fossil patch SUBCOMMAND ?ARGS ..? |
| @@ -545,10 +638,15 @@ | ||
| 545 | 638 | ** > fossil patch pull REMOTE-CHECKOUT |
| 546 | 639 | ** |
| 547 | 640 | ** Create a patch on a remote check-out, transfer that patch to the |
| 548 | 641 | ** local machine (using ssh) and apply the patch in the local checkout. |
| 549 | 642 | ** |
| 643 | +** -f|--force Apply the patch even though there are unsaved | |
| 644 | +** changes in the current check-out. | |
| 645 | +** -n|--dryrun Do nothing, but print what would have happened. | |
| 646 | +** -v|--verbose Extra output explaining what happens. | |
| 647 | +** | |
| 550 | 648 | ** > fossil patch view FILENAME |
| 551 | 649 | ** |
| 552 | 650 | ** View a summary of the the changes in the binary patch FILENAME. |
| 553 | 651 | ** |
| 554 | 652 | */ |
| @@ -560,75 +658,67 @@ | ||
| 560 | 658 | usage("apply|create|pull|push|view"); |
| 561 | 659 | } |
| 562 | 660 | zCmd = g.argv[2]; |
| 563 | 661 | n = strlen(zCmd); |
| 564 | 662 | if( strncmp(zCmd, "apply", n)==0 ){ |
| 565 | - int forceFlag = find_option("force","f",0)!=0; | |
| 566 | - unsigned flags = 0; | |
| 567 | - const char *zIn; | |
| 568 | - if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; | |
| 569 | - if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; | |
| 570 | - verify_all_options(); | |
| 571 | - if( g.argc!=4 && g.argc!=5 ){ | |
| 572 | - usage("apply [DIRECTORY] FILENAME"); | |
| 573 | - } | |
| 574 | - if( g.argc==5 ){ | |
| 575 | - file_chdir(g.argv[3], 0); | |
| 576 | - zIn = g.argv[4]; | |
| 577 | - }else{ | |
| 578 | - zIn = g.argv[3]; | |
| 579 | - } | |
| 580 | - db_must_be_within_tree(); | |
| 581 | - if( !forceFlag && unsaved_changes(0) ){ | |
| 582 | - fossil_fatal("there are unsaved changes in the current checkout"); | |
| 583 | - } | |
| 584 | - if( fossil_strcmp(zIn,"-")==0 ) zIn = 0; | |
| 585 | - patch_attach(zIn); | |
| 586 | - patch_apply(flags); | |
| 587 | - }else | |
| 588 | - if( strncmp(zCmd, "create", n)==0 ){ | |
| 589 | - const char *zOut; | |
| 590 | - verify_all_options(); | |
| 591 | - if( g.argc!=4 && g.argc!=5 ){ | |
| 592 | - usage("create [DIRECTORY] FILENAME"); | |
| 593 | - } | |
| 594 | - if( g.argc==5 ){ | |
| 595 | - file_chdir(g.argv[3], 0); | |
| 596 | - zOut = g.argv[4]; | |
| 597 | - }else{ | |
| 598 | - zOut = g.argv[3]; | |
| 599 | - } | |
| 600 | - if( fossil_strcmp(zOut, "-")==0 ) zOut = 0; | |
| 601 | - db_must_be_within_tree(); | |
| 602 | - patch_create(zOut); | |
| 603 | - }else | |
| 604 | - if( strncmp(zCmd, "pull", n)==0 ){ | |
| 605 | - db_must_be_within_tree(); | |
| 606 | - verify_all_options(); | |
| 607 | - if( g.argc!=4 ){ | |
| 608 | - usage("pull REMOTE-CHECKOUT"); | |
| 609 | - } | |
| 610 | - fossil_print("TBD...\n"); | |
| 611 | - }else | |
| 612 | - if( strncmp(zCmd, "push", n)==0 ){ | |
| 613 | - db_must_be_within_tree(); | |
| 614 | - verify_all_options(); | |
| 615 | - if( g.argc!=4 ){ | |
| 616 | - usage("push REMOTE-CHECKOUT"); | |
| 617 | - } | |
| 618 | - fossil_print("TBD...\n"); | |
| 663 | + char *zIn; | |
| 664 | + unsigned flags = 0; | |
| 665 | + if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; | |
| 666 | + if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; | |
| 667 | + if( find_option("force","f",0) ) flags |= PATCH_FORCE; | |
| 668 | + zIn = patch_find_patch_filename("apply"); | |
| 669 | + db_must_be_within_tree(); | |
| 670 | + patch_attach(zIn, stdin); | |
| 671 | + patch_apply(flags); | |
| 672 | + fossil_free(zIn); | |
| 673 | + }else | |
| 674 | + if( strncmp(zCmd, "create", n)==0 ){ | |
| 675 | + char *zOut; | |
| 676 | + zOut = patch_find_patch_filename("create"); | |
| 677 | + db_must_be_within_tree(); | |
| 678 | + patch_create(zOut, stdout); | |
| 679 | + fossil_free(zOut); | |
| 680 | + }else | |
| 681 | + if( strncmp(zCmd, "pull", n)==0 ){ | |
| 682 | + FILE *pIn = 0; | |
| 683 | + unsigned flags = 0; | |
| 684 | + if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; | |
| 685 | + if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; | |
| 686 | + if( find_option("force","f",0) ) flags |= PATCH_FORCE; | |
| 687 | + db_must_be_within_tree(); | |
| 688 | + verify_all_options(); | |
| 689 | + pIn = patch_remote_command(flags & (~PATCH_FORCE), "pull", "create", "r"); | |
| 690 | + if( pIn ){ | |
| 691 | + patch_attach(0, pIn); | |
| 692 | + pclose(pIn); | |
| 693 | + patch_apply(flags); | |
| 694 | + } | |
| 695 | + }else | |
| 696 | + if( strncmp(zCmd, "push", n)==0 ){ | |
| 697 | + FILE *pOut = 0; | |
| 698 | + unsigned flags = 0; | |
| 699 | + if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; | |
| 700 | + if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; | |
| 701 | + if( find_option("force","f",0) ) flags |= PATCH_FORCE; | |
| 702 | + db_must_be_within_tree(); | |
| 703 | + verify_all_options(); | |
| 704 | + pOut = patch_remote_command(flags, "push", "apply", "w"); | |
| 705 | + if( pOut ){ | |
| 706 | + patch_create(0, pOut); | |
| 707 | + pclose(pOut); | |
| 708 | + } | |
| 619 | 709 | }else |
| 620 | 710 | if( strncmp(zCmd, "view", n)==0 ){ |
| 621 | 711 | const char *zIn; |
| 622 | 712 | verify_all_options(); |
| 623 | 713 | if( g.argc!=4 ){ |
| 624 | 714 | usage("view FILENAME"); |
| 625 | 715 | } |
| 626 | 716 | zIn = g.argv[3]; |
| 627 | 717 | if( fossil_strcmp(zIn, "-")==0 ) zIn = 0; |
| 628 | - patch_attach(zIn); | |
| 718 | + patch_attach(zIn, stdin); | |
| 629 | 719 | patch_view(); |
| 630 | 720 | }else |
| 631 | 721 | { |
| 632 | 722 | goto patch_usage; |
| 633 | 723 | } |
| 634 | 724 | } |
| 635 | 725 |
| --- src/patch.c | |
| +++ src/patch.c | |
| @@ -19,16 +19,26 @@ | |
| 19 | */ |
| 20 | #include "config.h" |
| 21 | #include "patch.h" |
| 22 | #include <assert.h> |
| 23 | |
| 24 | /* |
| 25 | ** Flags passed from the main patch_cmd() routine into subfunctions used |
| 26 | ** to implement the various subcommands. |
| 27 | */ |
| 28 | #define PATCH_DRYRUN 0x0001 |
| 29 | #define PATCH_VERBOSE 0x0002 |
| 30 | |
| 31 | /* |
| 32 | ** Implementation of the "readfile(X)" SQL function. The entire content |
| 33 | ** of the checkout file named X is read and returned as a BLOB. |
| 34 | */ |
| @@ -113,11 +123,11 @@ | |
| 113 | |
| 114 | /* |
| 115 | ** Generate a binary patch file and store it into the file |
| 116 | ** named zOut. |
| 117 | */ |
| 118 | void patch_create(const char *zOut){ |
| 119 | int vid; |
| 120 | |
| 121 | if( zOut && file_isdir(zOut, ExtFILE)!=0 ){ |
| 122 | fossil_fatal("patch file already exists: %s", zOut); |
| 123 | } |
| @@ -147,13 +157,10 @@ | |
| 147 | vid = db_lget_int("checkout", 0); |
| 148 | vfile_check_signature(vid, CKSIG_ENOTFILE); |
| 149 | db_multi_exec( |
| 150 | "INSERT INTO patch.cfg(key,value)" |
| 151 | "SELECT 'baseline',uuid FROM blob WHERE rid=%d", vid); |
| 152 | if( db_exists("SELECT 1 FROM vmerge") ){ |
| 153 | db_multi_exec("INSERT INTO patch.cfg(key,value)VALUES('merged',1);"); |
| 154 | } |
| 155 | |
| 156 | /* New files */ |
| 157 | db_multi_exec( |
| 158 | "INSERT INTO patch.chng(pathname,hash,isexe,islink,delta)" |
| 159 | " SELECT pathname, NULL, isexe, islink," |
| @@ -198,53 +205,38 @@ | |
| 198 | unsigned char *pData; |
| 199 | pData = sqlite3_serialize(g.db, "patch", &sz, 0); |
| 200 | if( pData==0 ){ |
| 201 | fossil_fatal("out of memory"); |
| 202 | } |
| 203 | fossil_print("%025lld\n", sz); |
| 204 | fwrite(pData, sz, 1, stdout); |
| 205 | sqlite3_free(pData); |
| 206 | fflush(stdout); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /* |
| 211 | ** Attempt to load and validate a patchfile identified by the first |
| 212 | ** argument. |
| 213 | */ |
| 214 | void patch_attach(const char *zIn){ |
| 215 | Stmt q; |
| 216 | if( g.db==0 ){ |
| 217 | sqlite3_open(":memory:", &g.db); |
| 218 | } |
| 219 | if( zIn==0 ){ |
| 220 | char zBuf[26]; |
| 221 | size_t n; |
| 222 | sqlite3_int64 sz; |
| 223 | unsigned char *aIn; |
| 224 | |
| 225 | n = fread(zBuf, sizeof(zBuf), 1, stdin); |
| 226 | if( n!=1 ){ |
| 227 | fossil_fatal("unable to read size of input\n"); |
| 228 | } |
| 229 | zBuf[sizeof(zBuf)-1] = 0; |
| 230 | sz = atoll(zBuf); |
| 231 | if( sz<512 || (sz%512)!=0 ){ |
| 232 | fossil_fatal("bad size for input: %lld", sz); |
| 233 | } |
| 234 | aIn = sqlite3_malloc64( sz ); |
| 235 | if( aIn==0 ){ |
| 236 | fossil_fatal("out of memory"); |
| 237 | } |
| 238 | n = fread(aIn, 1, sz, stdin); |
| 239 | if( n!=sz ){ |
| 240 | fossil_fatal("got only %lld of %lld input bytes", |
| 241 | (sqlite3_int64)n, sz); |
| 242 | } |
| 243 | db_multi_exec("ATTACH ':memory:' AS patch"); |
| 244 | sqlite3_deserialize(g.db, "patch", aIn, sz, sz, |
| 245 | SQLITE_DESERIALIZE_FREEONCLOSE); |
| 246 | }else if( !file_isfile(zIn, ExtFILE) ){ |
| 247 | fossil_fatal("no such file: %s", zIn); |
| 248 | }else{ |
| 249 | db_multi_exec("ATTACH %Q AS patch", zIn); |
| 250 | } |
| @@ -310,10 +302,14 @@ | |
| 310 | ** and update all files. |
| 311 | */ |
| 312 | void patch_apply(unsigned mFlags){ |
| 313 | Stmt q; |
| 314 | Blob cmd; |
| 315 | blob_init(&cmd, 0, 0); |
| 316 | file_chdir(g.zLocalRoot, 0); |
| 317 | db_prepare(&q, |
| 318 | "SELECT patch.cfg.value" |
| 319 | " FROM patch.cfg, localdb.vvar" |
| @@ -503,10 +499,107 @@ | |
| 503 | } |
| 504 | blob_reset(&cmd); |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | |
| 509 | /* |
| 510 | ** COMMAND: patch |
| 511 | ** |
| 512 | ** Usage: %fossil patch SUBCOMMAND ?ARGS ..? |
| @@ -545,10 +638,15 @@ | |
| 545 | ** > fossil patch pull REMOTE-CHECKOUT |
| 546 | ** |
| 547 | ** Create a patch on a remote check-out, transfer that patch to the |
| 548 | ** local machine (using ssh) and apply the patch in the local checkout. |
| 549 | ** |
| 550 | ** > fossil patch view FILENAME |
| 551 | ** |
| 552 | ** View a summary of the the changes in the binary patch FILENAME. |
| 553 | ** |
| 554 | */ |
| @@ -560,75 +658,67 @@ | |
| 560 | usage("apply|create|pull|push|view"); |
| 561 | } |
| 562 | zCmd = g.argv[2]; |
| 563 | n = strlen(zCmd); |
| 564 | if( strncmp(zCmd, "apply", n)==0 ){ |
| 565 | int forceFlag = find_option("force","f",0)!=0; |
| 566 | unsigned flags = 0; |
| 567 | const char *zIn; |
| 568 | if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; |
| 569 | if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; |
| 570 | verify_all_options(); |
| 571 | if( g.argc!=4 && g.argc!=5 ){ |
| 572 | usage("apply [DIRECTORY] FILENAME"); |
| 573 | } |
| 574 | if( g.argc==5 ){ |
| 575 | file_chdir(g.argv[3], 0); |
| 576 | zIn = g.argv[4]; |
| 577 | }else{ |
| 578 | zIn = g.argv[3]; |
| 579 | } |
| 580 | db_must_be_within_tree(); |
| 581 | if( !forceFlag && unsaved_changes(0) ){ |
| 582 | fossil_fatal("there are unsaved changes in the current checkout"); |
| 583 | } |
| 584 | if( fossil_strcmp(zIn,"-")==0 ) zIn = 0; |
| 585 | patch_attach(zIn); |
| 586 | patch_apply(flags); |
| 587 | }else |
| 588 | if( strncmp(zCmd, "create", n)==0 ){ |
| 589 | const char *zOut; |
| 590 | verify_all_options(); |
| 591 | if( g.argc!=4 && g.argc!=5 ){ |
| 592 | usage("create [DIRECTORY] FILENAME"); |
| 593 | } |
| 594 | if( g.argc==5 ){ |
| 595 | file_chdir(g.argv[3], 0); |
| 596 | zOut = g.argv[4]; |
| 597 | }else{ |
| 598 | zOut = g.argv[3]; |
| 599 | } |
| 600 | if( fossil_strcmp(zOut, "-")==0 ) zOut = 0; |
| 601 | db_must_be_within_tree(); |
| 602 | patch_create(zOut); |
| 603 | }else |
| 604 | if( strncmp(zCmd, "pull", n)==0 ){ |
| 605 | db_must_be_within_tree(); |
| 606 | verify_all_options(); |
| 607 | if( g.argc!=4 ){ |
| 608 | usage("pull REMOTE-CHECKOUT"); |
| 609 | } |
| 610 | fossil_print("TBD...\n"); |
| 611 | }else |
| 612 | if( strncmp(zCmd, "push", n)==0 ){ |
| 613 | db_must_be_within_tree(); |
| 614 | verify_all_options(); |
| 615 | if( g.argc!=4 ){ |
| 616 | usage("push REMOTE-CHECKOUT"); |
| 617 | } |
| 618 | fossil_print("TBD...\n"); |
| 619 | }else |
| 620 | if( strncmp(zCmd, "view", n)==0 ){ |
| 621 | const char *zIn; |
| 622 | verify_all_options(); |
| 623 | if( g.argc!=4 ){ |
| 624 | usage("view FILENAME"); |
| 625 | } |
| 626 | zIn = g.argv[3]; |
| 627 | if( fossil_strcmp(zIn, "-")==0 ) zIn = 0; |
| 628 | patch_attach(zIn); |
| 629 | patch_view(); |
| 630 | }else |
| 631 | { |
| 632 | goto patch_usage; |
| 633 | } |
| 634 | } |
| 635 |
| --- src/patch.c | |
| +++ src/patch.c | |
| @@ -19,16 +19,26 @@ | |
| 19 | */ |
| 20 | #include "config.h" |
| 21 | #include "patch.h" |
| 22 | #include <assert.h> |
| 23 | |
| 24 | /* |
| 25 | ** Additional windows configuration for popen */ |
| 26 | #if defined(_WIN32) |
| 27 | # include <io.h> |
| 28 | # include <fcntl.h> |
| 29 | # undef popen |
| 30 | # define popen _popen |
| 31 | #endif |
| 32 | |
| 33 | /* |
| 34 | ** Flags passed from the main patch_cmd() routine into subfunctions used |
| 35 | ** to implement the various subcommands. |
| 36 | */ |
| 37 | #define PATCH_DRYRUN 0x0001 |
| 38 | #define PATCH_VERBOSE 0x0002 |
| 39 | #define PATCH_FORCE 0x0004 |
| 40 | |
| 41 | /* |
| 42 | ** Implementation of the "readfile(X)" SQL function. The entire content |
| 43 | ** of the checkout file named X is read and returned as a BLOB. |
| 44 | */ |
| @@ -113,11 +123,11 @@ | |
| 123 | |
| 124 | /* |
| 125 | ** Generate a binary patch file and store it into the file |
| 126 | ** named zOut. |
| 127 | */ |
| 128 | void patch_create(const char *zOut, FILE *out){ |
| 129 | int vid; |
| 130 | |
| 131 | if( zOut && file_isdir(zOut, ExtFILE)!=0 ){ |
| 132 | fossil_fatal("patch file already exists: %s", zOut); |
| 133 | } |
| @@ -147,13 +157,10 @@ | |
| 157 | vid = db_lget_int("checkout", 0); |
| 158 | vfile_check_signature(vid, CKSIG_ENOTFILE); |
| 159 | db_multi_exec( |
| 160 | "INSERT INTO patch.cfg(key,value)" |
| 161 | "SELECT 'baseline',uuid FROM blob WHERE rid=%d", vid); |
| 162 | |
| 163 | /* New files */ |
| 164 | db_multi_exec( |
| 165 | "INSERT INTO patch.chng(pathname,hash,isexe,islink,delta)" |
| 166 | " SELECT pathname, NULL, isexe, islink," |
| @@ -198,53 +205,38 @@ | |
| 205 | unsigned char *pData; |
| 206 | pData = sqlite3_serialize(g.db, "patch", &sz, 0); |
| 207 | if( pData==0 ){ |
| 208 | fossil_fatal("out of memory"); |
| 209 | } |
| 210 | fwrite(pData, sz, 1, out); |
| 211 | sqlite3_free(pData); |
| 212 | fflush(out); |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | /* |
| 217 | ** Attempt to load and validate a patchfile identified by the first |
| 218 | ** argument. |
| 219 | */ |
| 220 | void patch_attach(const char *zIn, FILE *in){ |
| 221 | Stmt q; |
| 222 | if( g.db==0 ){ |
| 223 | sqlite3_open(":memory:", &g.db); |
| 224 | } |
| 225 | if( zIn==0 ){ |
| 226 | Blob buf; |
| 227 | int rc; |
| 228 | int sz; |
| 229 | const unsigned char *pData; |
| 230 | blob_init(&buf, 0, 0); |
| 231 | sz = blob_read_from_channel(&buf, in, -1); |
| 232 | pData = (const unsigned char*)blob_buffer(&buf); |
| 233 | db_multi_exec("ATTACH ':memory:' AS patch"); |
| 234 | rc = sqlite3_deserialize(g.db, "patch", pData, sz, sz, 0); |
| 235 | if( rc ){ |
| 236 | fossil_fatal("cannot open patch database: %s", sqlite3_errmsg(g.db)); |
| 237 | } |
| 238 | }else if( !file_isfile(zIn, ExtFILE) ){ |
| 239 | fossil_fatal("no such file: %s", zIn); |
| 240 | }else{ |
| 241 | db_multi_exec("ATTACH %Q AS patch", zIn); |
| 242 | } |
| @@ -310,10 +302,14 @@ | |
| 302 | ** and update all files. |
| 303 | */ |
| 304 | void patch_apply(unsigned mFlags){ |
| 305 | Stmt q; |
| 306 | Blob cmd; |
| 307 | |
| 308 | if( (mFlags & PATCH_FORCE)==0 && unsaved_changes(0) ){ |
| 309 | fossil_fatal("there are unsaved changes in the current checkout"); |
| 310 | } |
| 311 | blob_init(&cmd, 0, 0); |
| 312 | file_chdir(g.zLocalRoot, 0); |
| 313 | db_prepare(&q, |
| 314 | "SELECT patch.cfg.value" |
| 315 | " FROM patch.cfg, localdb.vvar" |
| @@ -503,10 +499,107 @@ | |
| 499 | } |
| 500 | blob_reset(&cmd); |
| 501 | } |
| 502 | } |
| 503 | |
| 504 | /* |
| 505 | ** Find the filename of the patch file to be used by |
| 506 | ** "fossil patch apply" or "fossil patch create". |
| 507 | ** |
| 508 | ** If the name is "-" return NULL. |
| 509 | ** |
| 510 | ** Otherwise, if there is a prior DIRECTORY argument, or if |
| 511 | ** the --dir64 option is present, first chdir to the specified |
| 512 | ** directory, and translate the name in the argument accordingly. |
| 513 | ** |
| 514 | ** |
| 515 | ** The returned name is obtained from fossil_malloc() and should |
| 516 | ** be freed by the caller. |
| 517 | */ |
| 518 | static char *patch_find_patch_filename(const char *zCmdName){ |
| 519 | const char *zDir64 = find_option("dir64",0,1); |
| 520 | const char *zDir = 0; |
| 521 | const char *zBaseName; |
| 522 | char *zToFree = 0; |
| 523 | char *zPatchFile = 0; |
| 524 | if( zDir64 ){ |
| 525 | zToFree = decode64(zDir64, 0); |
| 526 | zDir = zToFree; |
| 527 | } |
| 528 | verify_all_options(); |
| 529 | if( g.argc!=4 && g.argc!=5 ){ |
| 530 | usage(mprintf("%s [DIRECTORY] FILENAME", zCmdName)); |
| 531 | } |
| 532 | if( g.argc==5 ){ |
| 533 | zDir = g.argv[3]; |
| 534 | zBaseName = g.argv[4]; |
| 535 | }else{ |
| 536 | zBaseName = g.argv[3]; |
| 537 | } |
| 538 | if( fossil_strcmp(zBaseName, "-")==0 ){ |
| 539 | zPatchFile = 0; |
| 540 | }else if( zDir ){ |
| 541 | zPatchFile = file_canonical_name_dup(g.argv[4]); |
| 542 | }else{ |
| 543 | zPatchFile = fossil_strdup(g.argv[3]); |
| 544 | } |
| 545 | if( zDir && file_chdir(zDir,0) ){ |
| 546 | fossil_fatal("cannot change to directory \"%s\"", zDir); |
| 547 | } |
| 548 | fossil_free(zToFree); |
| 549 | return zPatchFile; |
| 550 | } |
| 551 | |
| 552 | /* |
| 553 | ** Create a FILE* that will execute the remote side of a push or pull |
| 554 | ** using ssh (probably) or fossil for local pushes and pulls. Return |
| 555 | */ |
| 556 | static FILE *patch_remote_command( |
| 557 | unsigned mFlags, /* flags */ |
| 558 | const char *zThisCmd, /* "push" or "pull" */ |
| 559 | const char *zRemoteCmd, /* "apply" or "create" */ |
| 560 | const char *zRW /* "w" or "r" */ |
| 561 | ){ |
| 562 | char *zRemote; |
| 563 | char *zDir; |
| 564 | Blob cmd; |
| 565 | FILE *f; |
| 566 | const char *zForce = (mFlags & PATCH_FORCE)!=0 ? " -f" : ""; |
| 567 | if( g.argc!=4 ){ |
| 568 | usage(mprintf("%s [USER@]HOST:DIRECTORY", zThisCmd)); |
| 569 | } |
| 570 | zRemote = fossil_strdup(g.argv[3]); |
| 571 | zDir = strchr(zRemote,':'); |
| 572 | if( zDir==0 ){ |
| 573 | zDir = zRemote; |
| 574 | blob_init(&cmd, 0, 0); |
| 575 | blob_append_escaped_arg(&cmd, g.nameOfExe); |
| 576 | blob_appendf(&cmd, " patch %s%s %$ -", zRemoteCmd, zForce, zDir); |
| 577 | }else{ |
| 578 | Blob remote; |
| 579 | zDir[0] = 0; |
| 580 | zDir++; |
| 581 | transport_ssh_command(&cmd); |
| 582 | blob_append_escaped_arg(&cmd, zRemote); |
| 583 | blob_init(&remote, 0, 0); |
| 584 | blob_appendf(&remote, "fossil patch %s%s --dir64 %z -", |
| 585 | zRemoteCmd, zForce, encode64(zDir, -1)); |
| 586 | blob_append_escaped_arg(&cmd, blob_str(&remote)); |
| 587 | blob_reset(&remote); |
| 588 | } |
| 589 | if( mFlags & PATCH_VERBOSE ){ |
| 590 | fossil_print("# %s\n", blob_str(&cmd)); |
| 591 | fflush(stdout); |
| 592 | } |
| 593 | f = popen(blob_str(&cmd), zRW); |
| 594 | if( f==0 ){ |
| 595 | fossil_fatal("cannot run command: %s", blob_str(&cmd)); |
| 596 | } |
| 597 | blob_reset(&cmd); |
| 598 | return f; |
| 599 | } |
| 600 | |
| 601 | |
| 602 | /* |
| 603 | ** COMMAND: patch |
| 604 | ** |
| 605 | ** Usage: %fossil patch SUBCOMMAND ?ARGS ..? |
| @@ -545,10 +638,15 @@ | |
| 638 | ** > fossil patch pull REMOTE-CHECKOUT |
| 639 | ** |
| 640 | ** Create a patch on a remote check-out, transfer that patch to the |
| 641 | ** local machine (using ssh) and apply the patch in the local checkout. |
| 642 | ** |
| 643 | ** -f|--force Apply the patch even though there are unsaved |
| 644 | ** changes in the current check-out. |
| 645 | ** -n|--dryrun Do nothing, but print what would have happened. |
| 646 | ** -v|--verbose Extra output explaining what happens. |
| 647 | ** |
| 648 | ** > fossil patch view FILENAME |
| 649 | ** |
| 650 | ** View a summary of the the changes in the binary patch FILENAME. |
| 651 | ** |
| 652 | */ |
| @@ -560,75 +658,67 @@ | |
| 658 | usage("apply|create|pull|push|view"); |
| 659 | } |
| 660 | zCmd = g.argv[2]; |
| 661 | n = strlen(zCmd); |
| 662 | if( strncmp(zCmd, "apply", n)==0 ){ |
| 663 | char *zIn; |
| 664 | unsigned flags = 0; |
| 665 | if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; |
| 666 | if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; |
| 667 | if( find_option("force","f",0) ) flags |= PATCH_FORCE; |
| 668 | zIn = patch_find_patch_filename("apply"); |
| 669 | db_must_be_within_tree(); |
| 670 | patch_attach(zIn, stdin); |
| 671 | patch_apply(flags); |
| 672 | fossil_free(zIn); |
| 673 | }else |
| 674 | if( strncmp(zCmd, "create", n)==0 ){ |
| 675 | char *zOut; |
| 676 | zOut = patch_find_patch_filename("create"); |
| 677 | db_must_be_within_tree(); |
| 678 | patch_create(zOut, stdout); |
| 679 | fossil_free(zOut); |
| 680 | }else |
| 681 | if( strncmp(zCmd, "pull", n)==0 ){ |
| 682 | FILE *pIn = 0; |
| 683 | unsigned flags = 0; |
| 684 | if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; |
| 685 | if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; |
| 686 | if( find_option("force","f",0) ) flags |= PATCH_FORCE; |
| 687 | db_must_be_within_tree(); |
| 688 | verify_all_options(); |
| 689 | pIn = patch_remote_command(flags & (~PATCH_FORCE), "pull", "create", "r"); |
| 690 | if( pIn ){ |
| 691 | patch_attach(0, pIn); |
| 692 | pclose(pIn); |
| 693 | patch_apply(flags); |
| 694 | } |
| 695 | }else |
| 696 | if( strncmp(zCmd, "push", n)==0 ){ |
| 697 | FILE *pOut = 0; |
| 698 | unsigned flags = 0; |
| 699 | if( find_option("dryrun","n",0) ) flags |= PATCH_DRYRUN; |
| 700 | if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; |
| 701 | if( find_option("force","f",0) ) flags |= PATCH_FORCE; |
| 702 | db_must_be_within_tree(); |
| 703 | verify_all_options(); |
| 704 | pOut = patch_remote_command(flags, "push", "apply", "w"); |
| 705 | if( pOut ){ |
| 706 | patch_create(0, pOut); |
| 707 | pclose(pOut); |
| 708 | } |
| 709 | }else |
| 710 | if( strncmp(zCmd, "view", n)==0 ){ |
| 711 | const char *zIn; |
| 712 | verify_all_options(); |
| 713 | if( g.argc!=4 ){ |
| 714 | usage("view FILENAME"); |
| 715 | } |
| 716 | zIn = g.argv[3]; |
| 717 | if( fossil_strcmp(zIn, "-")==0 ) zIn = 0; |
| 718 | patch_attach(zIn, stdin); |
| 719 | patch_view(); |
| 720 | }else |
| 721 | { |
| 722 | goto patch_usage; |
| 723 | } |
| 724 | } |
| 725 |