Fossil SCM
Collected improvements to the fossil wiki and attach commands to support technotes as well as wiki pages, including attachments to either and new test cases. Passes all tests.
Commit
05cd9fa2bfd7656b561226945275b606f5c42786
Parent
45daaced43d792a…
7 files changed
+162
-50
+4
-22
+30
-7
+2
-1
+11
-6
+162
-86
+334
+162
-50
| --- src/attach.c | ||
| +++ src/attach.c | ||
| @@ -246,10 +246,66 @@ | ||
| 246 | 246 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid); |
| 247 | 247 | } |
| 248 | 248 | manifest_crosslink(rid, pAttach, MC_NONE); |
| 249 | 249 | } |
| 250 | 250 | |
| 251 | + | |
| 252 | +/* | |
| 253 | +** Commit a new attachment into the repository | |
| 254 | +*/ | |
| 255 | +void attach_commit( | |
| 256 | + const char *zName, /* The filename of the attachment */ | |
| 257 | + const char *zTarget, /* The artifact uuid to attach to */ | |
| 258 | + const char *aContent, /* The content of the attachment */ | |
| 259 | + int szContent, /* The length of the attachment */ | |
| 260 | + int needModerator, /* Moderate the attachment? */ | |
| 261 | + const char *zComment /* The comment for the attachment */ | |
| 262 | +){ | |
| 263 | + Blob content; | |
| 264 | + Blob manifest; | |
| 265 | + Blob cksum; | |
| 266 | + char *zUUID; | |
| 267 | + char *zDate; | |
| 268 | + int rid; | |
| 269 | + int i, n; | |
| 270 | + int addCompress = 0; | |
| 271 | + Manifest *pManifest; | |
| 272 | + | |
| 273 | + db_begin_transaction(); | |
| 274 | + blob_init(&content, aContent, szContent); | |
| 275 | + pManifest = manifest_parse(&content, 0, 0); | |
| 276 | + manifest_destroy(pManifest); | |
| 277 | + blob_init(&content, aContent, szContent); | |
| 278 | + if( pManifest ){ | |
| 279 | + blob_compress(&content, &content); | |
| 280 | + addCompress = 1; | |
| 281 | + } | |
| 282 | + rid = content_put_ex(&content, 0, 0, 0, needModerator); | |
| 283 | + zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 284 | + blob_zero(&manifest); | |
| 285 | + for(i=n=0; zName[i]; i++){ | |
| 286 | + if( zName[i]=='/' || zName[i]=='\\' ) n = i+1; | |
| 287 | + } | |
| 288 | + zName += n; | |
| 289 | + if( zName[0]==0 ) zName = "unknown"; | |
| 290 | + blob_appendf(&manifest, "A %F%s %F %s\n", | |
| 291 | + zName, addCompress ? ".gz" : "", zTarget, zUUID); | |
| 292 | + while( fossil_isspace(zComment[0]) ) zComment++; | |
| 293 | + n = strlen(zComment); | |
| 294 | + while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } | |
| 295 | + if( n>0 ){ | |
| 296 | + blob_appendf(&manifest, "C %#F\n", n, zComment); | |
| 297 | + } | |
| 298 | + zDate = date_in_standard_format("now"); | |
| 299 | + blob_appendf(&manifest, "D %s\n", zDate); | |
| 300 | + blob_appendf(&manifest, "U %F\n", login_name()); | |
| 301 | + md5sum_blob(&manifest, &cksum); | |
| 302 | + blob_appendf(&manifest, "Z %b\n", &cksum); | |
| 303 | + attach_put(&manifest, rid, needModerator); | |
| 304 | + assert( blob_is_reset(&manifest) ); | |
| 305 | + db_end_transaction(0); | |
| 306 | +} | |
| 251 | 307 | |
| 252 | 308 | /* |
| 253 | 309 | ** WEBPAGE: attachadd |
| 254 | 310 | ** Add a new attachment. |
| 255 | 311 | ** |
| @@ -300,11 +356,11 @@ | ||
| 300 | 356 | zTechNote = db_text(0, "SELECT substr(tagname,7) FROM tag" |
| 301 | 357 | " WHERE tagname GLOB 'event-%q*'", zTechNote); |
| 302 | 358 | if( zTechNote==0) fossil_redirect_home(); |
| 303 | 359 | } |
| 304 | 360 | zTarget = zTechNote; |
| 305 | - zTargetType = mprintf("Tech Note <a href=\"%R/technote/%h\">%h</a>", | |
| 361 | + zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>", | |
| 306 | 362 | zTechNote, zTechNote); |
| 307 | 363 | |
| 308 | 364 | }else{ |
| 309 | 365 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 310 | 366 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| @@ -322,59 +378,14 @@ | ||
| 322 | 378 | if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop); |
| 323 | 379 | if( P("cancel") ){ |
| 324 | 380 | cgi_redirect(zFrom); |
| 325 | 381 | } |
| 326 | 382 | if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){ |
| 327 | - Blob content; | |
| 328 | - Blob manifest; | |
| 329 | - Blob cksum; | |
| 330 | - char *zUUID; | |
| 331 | - const char *zComment; | |
| 332 | - char *zDate; | |
| 333 | - int rid; | |
| 334 | - int i, n; | |
| 335 | - int addCompress = 0; | |
| 336 | - Manifest *pManifest; | |
| 337 | - int needModerator; | |
| 338 | - | |
| 339 | - db_begin_transaction(); | |
| 340 | - blob_init(&content, aContent, szContent); | |
| 341 | - pManifest = manifest_parse(&content, 0, 0); | |
| 342 | - manifest_destroy(pManifest); | |
| 343 | - blob_init(&content, aContent, szContent); | |
| 344 | - if( pManifest ){ | |
| 345 | - blob_compress(&content, &content); | |
| 346 | - addCompress = 1; | |
| 347 | - } | |
| 348 | - needModerator = | |
| 349 | - (zTkt!=0 && ticket_need_moderation(0)) || | |
| 350 | - (zPage!=0 && wiki_need_moderation(0)); | |
| 351 | - rid = content_put_ex(&content, 0, 0, 0, needModerator); | |
| 352 | - zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 353 | - blob_zero(&manifest); | |
| 354 | - for(i=n=0; zName[i]; i++){ | |
| 355 | - if( zName[i]=='/' || zName[i]=='\\' ) n = i; | |
| 356 | - } | |
| 357 | - zName += n; | |
| 358 | - if( zName[0]==0 ) zName = "unknown"; | |
| 359 | - blob_appendf(&manifest, "A %F%s %F %s\n", | |
| 360 | - zName, addCompress ? ".gz" : "", zTarget, zUUID); | |
| 361 | - zComment = PD("comment", ""); | |
| 362 | - while( fossil_isspace(zComment[0]) ) zComment++; | |
| 363 | - n = strlen(zComment); | |
| 364 | - while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } | |
| 365 | - if( n>0 ){ | |
| 366 | - blob_appendf(&manifest, "C %#F\n", n, zComment); | |
| 367 | - } | |
| 368 | - zDate = date_in_standard_format("now"); | |
| 369 | - blob_appendf(&manifest, "D %s\n", zDate); | |
| 370 | - blob_appendf(&manifest, "U %F\n", login_name()); | |
| 371 | - md5sum_blob(&manifest, &cksum); | |
| 372 | - blob_appendf(&manifest, "Z %b\n", &cksum); | |
| 373 | - attach_put(&manifest, rid, needModerator); | |
| 374 | - assert( blob_is_reset(&manifest) ); | |
| 375 | - db_end_transaction(0); | |
| 383 | + int needModerator = (zTkt!=0 && ticket_need_moderation(0)) || | |
| 384 | + (zPage!=0 && wiki_need_moderation(0)); | |
| 385 | + const char *zComment = PD("comment", ""); | |
| 386 | + attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); | |
| 376 | 387 | cgi_redirect(zFrom); |
| 377 | 388 | } |
| 378 | 389 | style_header("Add Attachment"); |
| 379 | 390 | if( !goodCaptcha ){ |
| 380 | 391 | @ <p class="generalError">Error: Incorrect security code.</p> |
| @@ -670,5 +681,106 @@ | ||
| 670 | 681 | @ </ul> |
| 671 | 682 | } |
| 672 | 683 | db_finalize(&q); |
| 673 | 684 | |
| 674 | 685 | } |
| 686 | + | |
| 687 | +/* | |
| 688 | +** COMMAND: attachment* | |
| 689 | +** | |
| 690 | +** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS? | |
| 691 | +** | |
| 692 | +** Add an attachment to an existing wiki page or tech note. | |
| 693 | +** | |
| 694 | +** Options: | |
| 695 | +** -t|--technote DATETIME Specifies the timestamp of | |
| 696 | +** the technote to which the attachment | |
| 697 | +** is to be made. The attachment will be | |
| 698 | +** to the most recently modified tech note | |
| 699 | +** with the specified timestamp. | |
| 700 | +** -t|--technote TECHNOTE-ID Specifies the technote to be | |
| 701 | +** updated by its technote id. | |
| 702 | +** | |
| 703 | +** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified. | |
| 704 | +*/ | |
| 705 | +void attachment_cmd(void){ | |
| 706 | + int n; | |
| 707 | + db_find_and_open_repository(0, 0); | |
| 708 | + if( g.argc<3 ){ | |
| 709 | + goto attachment_cmd_usage; | |
| 710 | + } | |
| 711 | + n = strlen(g.argv[2]); | |
| 712 | + if( n==0 ){ | |
| 713 | + goto attachment_cmd_usage; | |
| 714 | + } | |
| 715 | + | |
| 716 | + if( strncmp(g.argv[2],"add",n)==0 ){ | |
| 717 | + const char *zPageName; /* Name of the wiki page to attach to */ | |
| 718 | + const char *zFile; /* Name of the file to be attached */ | |
| 719 | + const char *zETime; /* The name of the technote to attach to */ | |
| 720 | + Manifest *pWiki = 0; /* Parsed wiki page content */ | |
| 721 | + char *zBody = 0; /* Wiki page content */ | |
| 722 | + int rid; | |
| 723 | + const char *zTarget; /* Target of the attachment */ | |
| 724 | + Blob content; /* The content of the attachment */ | |
| 725 | + zETime = find_option("technote","t",1); | |
| 726 | + if( !zETime ){ | |
| 727 | + if( g.argc!=5 ){ | |
| 728 | + usage("add PAGENAME FILENAME"); | |
| 729 | + } | |
| 730 | + zPageName = g.argv[3]; | |
| 731 | + rid = db_int(0, "SELECT x.rid FROM tag t, tagxref x" | |
| 732 | + " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" | |
| 733 | + " ORDER BY x.mtime DESC LIMIT 1", | |
| 734 | + zPageName | |
| 735 | + ); | |
| 736 | + if( (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){ | |
| 737 | + zBody = pWiki->zWiki; | |
| 738 | + } | |
| 739 | + if( zBody==0 ){ | |
| 740 | + fossil_fatal("wiki page [%s] not found",zPageName); | |
| 741 | + } | |
| 742 | + zTarget = zPageName; | |
| 743 | + zFile = g.argv[4]; | |
| 744 | + }else{ | |
| 745 | + if( g.argc!=4 ){ | |
| 746 | + usage("add FILENAME --technote DATETIME|TECHNOTE-ID"); | |
| 747 | + } | |
| 748 | + rid = wiki_technote_to_rid(zETime); | |
| 749 | + if( rid<0 ){ | |
| 750 | + fossil_fatal("ambiguous tech note id: %s", zETime); | |
| 751 | + } | |
| 752 | + if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ | |
| 753 | + zBody = pWiki->zWiki; | |
| 754 | + } | |
| 755 | + if( zBody==0 ){ | |
| 756 | + fossil_fatal("technote [%s] not found",zETime); | |
| 757 | + } | |
| 758 | + zTarget = db_text(0, | |
| 759 | + "SELECT substr(tagname,7) FROM tag WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", | |
| 760 | + rid | |
| 761 | + ); | |
| 762 | + zFile = g.argv[3]; | |
| 763 | + } | |
| 764 | + blob_read_from_file(&content, zFile); | |
| 765 | + user_select(); | |
| 766 | + attach_commit( | |
| 767 | + zFile, /* The filename of the attachment */ | |
| 768 | + zTarget, /* The artifact uuid to attach to */ | |
| 769 | + blob_buffer(&content), /* The content of the attachment */ | |
| 770 | + blob_size(&content), /* The length of the attachment */ | |
| 771 | + 0, /* No need to moderate the attachment */ | |
| 772 | + "" /* Empty attachment comment */ | |
| 773 | + ); | |
| 774 | + if( !zETime ){ | |
| 775 | + fossil_print("Attached %s to wiki page %s.\n", zFile, zPageName); | |
| 776 | + }else{ | |
| 777 | + fossil_print("Attached %s to tech note %s.\n", zFile, zETime); | |
| 778 | + } | |
| 779 | + }else{ | |
| 780 | + goto attachment_cmd_usage; | |
| 781 | + } | |
| 782 | + return; | |
| 783 | + | |
| 784 | +attachment_cmd_usage: | |
| 785 | + usage("add ?PAGENAME? FILENAME [-t|--technote DATETIME ]"); | |
| 786 | +} | |
| 675 | 787 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -246,10 +246,66 @@ | |
| 246 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid); |
| 247 | } |
| 248 | manifest_crosslink(rid, pAttach, MC_NONE); |
| 249 | } |
| 250 | |
| 251 | |
| 252 | /* |
| 253 | ** WEBPAGE: attachadd |
| 254 | ** Add a new attachment. |
| 255 | ** |
| @@ -300,11 +356,11 @@ | |
| 300 | zTechNote = db_text(0, "SELECT substr(tagname,7) FROM tag" |
| 301 | " WHERE tagname GLOB 'event-%q*'", zTechNote); |
| 302 | if( zTechNote==0) fossil_redirect_home(); |
| 303 | } |
| 304 | zTarget = zTechNote; |
| 305 | zTargetType = mprintf("Tech Note <a href=\"%R/technote/%h\">%h</a>", |
| 306 | zTechNote, zTechNote); |
| 307 | |
| 308 | }else{ |
| 309 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 310 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| @@ -322,59 +378,14 @@ | |
| 322 | if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop); |
| 323 | if( P("cancel") ){ |
| 324 | cgi_redirect(zFrom); |
| 325 | } |
| 326 | if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){ |
| 327 | Blob content; |
| 328 | Blob manifest; |
| 329 | Blob cksum; |
| 330 | char *zUUID; |
| 331 | const char *zComment; |
| 332 | char *zDate; |
| 333 | int rid; |
| 334 | int i, n; |
| 335 | int addCompress = 0; |
| 336 | Manifest *pManifest; |
| 337 | int needModerator; |
| 338 | |
| 339 | db_begin_transaction(); |
| 340 | blob_init(&content, aContent, szContent); |
| 341 | pManifest = manifest_parse(&content, 0, 0); |
| 342 | manifest_destroy(pManifest); |
| 343 | blob_init(&content, aContent, szContent); |
| 344 | if( pManifest ){ |
| 345 | blob_compress(&content, &content); |
| 346 | addCompress = 1; |
| 347 | } |
| 348 | needModerator = |
| 349 | (zTkt!=0 && ticket_need_moderation(0)) || |
| 350 | (zPage!=0 && wiki_need_moderation(0)); |
| 351 | rid = content_put_ex(&content, 0, 0, 0, needModerator); |
| 352 | zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 353 | blob_zero(&manifest); |
| 354 | for(i=n=0; zName[i]; i++){ |
| 355 | if( zName[i]=='/' || zName[i]=='\\' ) n = i; |
| 356 | } |
| 357 | zName += n; |
| 358 | if( zName[0]==0 ) zName = "unknown"; |
| 359 | blob_appendf(&manifest, "A %F%s %F %s\n", |
| 360 | zName, addCompress ? ".gz" : "", zTarget, zUUID); |
| 361 | zComment = PD("comment", ""); |
| 362 | while( fossil_isspace(zComment[0]) ) zComment++; |
| 363 | n = strlen(zComment); |
| 364 | while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } |
| 365 | if( n>0 ){ |
| 366 | blob_appendf(&manifest, "C %#F\n", n, zComment); |
| 367 | } |
| 368 | zDate = date_in_standard_format("now"); |
| 369 | blob_appendf(&manifest, "D %s\n", zDate); |
| 370 | blob_appendf(&manifest, "U %F\n", login_name()); |
| 371 | md5sum_blob(&manifest, &cksum); |
| 372 | blob_appendf(&manifest, "Z %b\n", &cksum); |
| 373 | attach_put(&manifest, rid, needModerator); |
| 374 | assert( blob_is_reset(&manifest) ); |
| 375 | db_end_transaction(0); |
| 376 | cgi_redirect(zFrom); |
| 377 | } |
| 378 | style_header("Add Attachment"); |
| 379 | if( !goodCaptcha ){ |
| 380 | @ <p class="generalError">Error: Incorrect security code.</p> |
| @@ -670,5 +681,106 @@ | |
| 670 | @ </ul> |
| 671 | } |
| 672 | db_finalize(&q); |
| 673 | |
| 674 | } |
| 675 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -246,10 +246,66 @@ | |
| 246 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid); |
| 247 | } |
| 248 | manifest_crosslink(rid, pAttach, MC_NONE); |
| 249 | } |
| 250 | |
| 251 | |
| 252 | /* |
| 253 | ** Commit a new attachment into the repository |
| 254 | */ |
| 255 | void attach_commit( |
| 256 | const char *zName, /* The filename of the attachment */ |
| 257 | const char *zTarget, /* The artifact uuid to attach to */ |
| 258 | const char *aContent, /* The content of the attachment */ |
| 259 | int szContent, /* The length of the attachment */ |
| 260 | int needModerator, /* Moderate the attachment? */ |
| 261 | const char *zComment /* The comment for the attachment */ |
| 262 | ){ |
| 263 | Blob content; |
| 264 | Blob manifest; |
| 265 | Blob cksum; |
| 266 | char *zUUID; |
| 267 | char *zDate; |
| 268 | int rid; |
| 269 | int i, n; |
| 270 | int addCompress = 0; |
| 271 | Manifest *pManifest; |
| 272 | |
| 273 | db_begin_transaction(); |
| 274 | blob_init(&content, aContent, szContent); |
| 275 | pManifest = manifest_parse(&content, 0, 0); |
| 276 | manifest_destroy(pManifest); |
| 277 | blob_init(&content, aContent, szContent); |
| 278 | if( pManifest ){ |
| 279 | blob_compress(&content, &content); |
| 280 | addCompress = 1; |
| 281 | } |
| 282 | rid = content_put_ex(&content, 0, 0, 0, needModerator); |
| 283 | zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 284 | blob_zero(&manifest); |
| 285 | for(i=n=0; zName[i]; i++){ |
| 286 | if( zName[i]=='/' || zName[i]=='\\' ) n = i+1; |
| 287 | } |
| 288 | zName += n; |
| 289 | if( zName[0]==0 ) zName = "unknown"; |
| 290 | blob_appendf(&manifest, "A %F%s %F %s\n", |
| 291 | zName, addCompress ? ".gz" : "", zTarget, zUUID); |
| 292 | while( fossil_isspace(zComment[0]) ) zComment++; |
| 293 | n = strlen(zComment); |
| 294 | while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } |
| 295 | if( n>0 ){ |
| 296 | blob_appendf(&manifest, "C %#F\n", n, zComment); |
| 297 | } |
| 298 | zDate = date_in_standard_format("now"); |
| 299 | blob_appendf(&manifest, "D %s\n", zDate); |
| 300 | blob_appendf(&manifest, "U %F\n", login_name()); |
| 301 | md5sum_blob(&manifest, &cksum); |
| 302 | blob_appendf(&manifest, "Z %b\n", &cksum); |
| 303 | attach_put(&manifest, rid, needModerator); |
| 304 | assert( blob_is_reset(&manifest) ); |
| 305 | db_end_transaction(0); |
| 306 | } |
| 307 | |
| 308 | /* |
| 309 | ** WEBPAGE: attachadd |
| 310 | ** Add a new attachment. |
| 311 | ** |
| @@ -300,11 +356,11 @@ | |
| 356 | zTechNote = db_text(0, "SELECT substr(tagname,7) FROM tag" |
| 357 | " WHERE tagname GLOB 'event-%q*'", zTechNote); |
| 358 | if( zTechNote==0) fossil_redirect_home(); |
| 359 | } |
| 360 | zTarget = zTechNote; |
| 361 | zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>", |
| 362 | zTechNote, zTechNote); |
| 363 | |
| 364 | }else{ |
| 365 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 366 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| @@ -322,59 +378,14 @@ | |
| 378 | if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop); |
| 379 | if( P("cancel") ){ |
| 380 | cgi_redirect(zFrom); |
| 381 | } |
| 382 | if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){ |
| 383 | int needModerator = (zTkt!=0 && ticket_need_moderation(0)) || |
| 384 | (zPage!=0 && wiki_need_moderation(0)); |
| 385 | const char *zComment = PD("comment", ""); |
| 386 | attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); |
| 387 | cgi_redirect(zFrom); |
| 388 | } |
| 389 | style_header("Add Attachment"); |
| 390 | if( !goodCaptcha ){ |
| 391 | @ <p class="generalError">Error: Incorrect security code.</p> |
| @@ -670,5 +681,106 @@ | |
| 681 | @ </ul> |
| 682 | } |
| 683 | db_finalize(&q); |
| 684 | |
| 685 | } |
| 686 | |
| 687 | /* |
| 688 | ** COMMAND: attachment* |
| 689 | ** |
| 690 | ** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS? |
| 691 | ** |
| 692 | ** Add an attachment to an existing wiki page or tech note. |
| 693 | ** |
| 694 | ** Options: |
| 695 | ** -t|--technote DATETIME Specifies the timestamp of |
| 696 | ** the technote to which the attachment |
| 697 | ** is to be made. The attachment will be |
| 698 | ** to the most recently modified tech note |
| 699 | ** with the specified timestamp. |
| 700 | ** -t|--technote TECHNOTE-ID Specifies the technote to be |
| 701 | ** updated by its technote id. |
| 702 | ** |
| 703 | ** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified. |
| 704 | */ |
| 705 | void attachment_cmd(void){ |
| 706 | int n; |
| 707 | db_find_and_open_repository(0, 0); |
| 708 | if( g.argc<3 ){ |
| 709 | goto attachment_cmd_usage; |
| 710 | } |
| 711 | n = strlen(g.argv[2]); |
| 712 | if( n==0 ){ |
| 713 | goto attachment_cmd_usage; |
| 714 | } |
| 715 | |
| 716 | if( strncmp(g.argv[2],"add",n)==0 ){ |
| 717 | const char *zPageName; /* Name of the wiki page to attach to */ |
| 718 | const char *zFile; /* Name of the file to be attached */ |
| 719 | const char *zETime; /* The name of the technote to attach to */ |
| 720 | Manifest *pWiki = 0; /* Parsed wiki page content */ |
| 721 | char *zBody = 0; /* Wiki page content */ |
| 722 | int rid; |
| 723 | const char *zTarget; /* Target of the attachment */ |
| 724 | Blob content; /* The content of the attachment */ |
| 725 | zETime = find_option("technote","t",1); |
| 726 | if( !zETime ){ |
| 727 | if( g.argc!=5 ){ |
| 728 | usage("add PAGENAME FILENAME"); |
| 729 | } |
| 730 | zPageName = g.argv[3]; |
| 731 | rid = db_int(0, "SELECT x.rid FROM tag t, tagxref x" |
| 732 | " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" |
| 733 | " ORDER BY x.mtime DESC LIMIT 1", |
| 734 | zPageName |
| 735 | ); |
| 736 | if( (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){ |
| 737 | zBody = pWiki->zWiki; |
| 738 | } |
| 739 | if( zBody==0 ){ |
| 740 | fossil_fatal("wiki page [%s] not found",zPageName); |
| 741 | } |
| 742 | zTarget = zPageName; |
| 743 | zFile = g.argv[4]; |
| 744 | }else{ |
| 745 | if( g.argc!=4 ){ |
| 746 | usage("add FILENAME --technote DATETIME|TECHNOTE-ID"); |
| 747 | } |
| 748 | rid = wiki_technote_to_rid(zETime); |
| 749 | if( rid<0 ){ |
| 750 | fossil_fatal("ambiguous tech note id: %s", zETime); |
| 751 | } |
| 752 | if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ |
| 753 | zBody = pWiki->zWiki; |
| 754 | } |
| 755 | if( zBody==0 ){ |
| 756 | fossil_fatal("technote [%s] not found",zETime); |
| 757 | } |
| 758 | zTarget = db_text(0, |
| 759 | "SELECT substr(tagname,7) FROM tag WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", |
| 760 | rid |
| 761 | ); |
| 762 | zFile = g.argv[3]; |
| 763 | } |
| 764 | blob_read_from_file(&content, zFile); |
| 765 | user_select(); |
| 766 | attach_commit( |
| 767 | zFile, /* The filename of the attachment */ |
| 768 | zTarget, /* The artifact uuid to attach to */ |
| 769 | blob_buffer(&content), /* The content of the attachment */ |
| 770 | blob_size(&content), /* The length of the attachment */ |
| 771 | 0, /* No need to moderate the attachment */ |
| 772 | "" /* Empty attachment comment */ |
| 773 | ); |
| 774 | if( !zETime ){ |
| 775 | fossil_print("Attached %s to wiki page %s.\n", zFile, zPageName); |
| 776 | }else{ |
| 777 | fossil_print("Attached %s to tech note %s.\n", zFile, zETime); |
| 778 | } |
| 779 | }else{ |
| 780 | goto attachment_cmd_usage; |
| 781 | } |
| 782 | return; |
| 783 | |
| 784 | attachment_cmd_usage: |
| 785 | usage("add ?PAGENAME? FILENAME [-t|--technote DATETIME ]"); |
| 786 | } |
| 787 |
+4
-22
| --- src/event.c | ||
| +++ src/event.c | ||
| @@ -540,44 +540,26 @@ | ||
| 540 | 540 | style_footer(); |
| 541 | 541 | } |
| 542 | 542 | |
| 543 | 543 | /* |
| 544 | 544 | ** Add a new tech note to the repository. The timestamp is |
| 545 | -** given by the zETime parameter. isNew must be true to create | |
| 545 | +** given by the zETime parameter. rid must be zero to create | |
| 546 | 546 | ** a new page. If no previous page with the name zPageName exists |
| 547 | 547 | ** and isNew is false, then this routine throws an error. |
| 548 | 548 | */ |
| 549 | 549 | void event_cmd_commit( |
| 550 | 550 | char *zETime, /* timestamp */ |
| 551 | - int isNew, /* true to create a new page */ | |
| 551 | + int rid, /* Artifact id of the tech note */ | |
| 552 | 552 | Blob *pContent, /* content of the new page */ |
| 553 | 553 | const char *zMimeType, /* mimetype of the content */ |
| 554 | 554 | const char *zComment, /* comment to go on the timeline */ |
| 555 | 555 | const char *zTags, /* tags */ |
| 556 | 556 | const char *zClr /* background color */ |
| 557 | 557 | ){ |
| 558 | - int rid; /* Artifact id of the tech note */ | |
| 559 | 558 | const char *zId; /* id of the tech note */ |
| 560 | - rid = db_int(0, "SELECT objid FROM event" | |
| 561 | - " WHERE datetime(mtime)=datetime('%q') AND type = 'e'" | |
| 562 | - " LIMIT 1", | |
| 563 | - zETime | |
| 564 | - ); | |
| 565 | - if( rid==0 && !isNew ){ | |
| 566 | -#ifdef FOSSIL_ENABLE_JSON | |
| 567 | - g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND; | |
| 568 | -#endif | |
| 569 | - fossil_fatal("no such tech note: %s", zETime); | |
| 570 | - } | |
| 571 | - if( rid!=0 && isNew ){ | |
| 572 | -#ifdef FOSSIL_ENABLE_JSON | |
| 573 | - g.json.resultCode = FSL_JSON_E_RESOURCE_ALREADY_EXISTS; | |
| 574 | -#endif | |
| 575 | - fossil_fatal("tech note %s already exists", zETime); | |
| 576 | - } | |
| 577 | - | |
| 578 | - if ( isNew ){ | |
| 559 | + | |
| 560 | + if ( rid==0 ){ | |
| 579 | 561 | zId = db_text(0, "SELECT lower(hex(randomblob(20)))"); |
| 580 | 562 | }else{ |
| 581 | 563 | zId = db_text(0, |
| 582 | 564 | "SELECT substr(tagname,7) FROM tag" |
| 583 | 565 | " WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", |
| 584 | 566 |
| --- src/event.c | |
| +++ src/event.c | |
| @@ -540,44 +540,26 @@ | |
| 540 | style_footer(); |
| 541 | } |
| 542 | |
| 543 | /* |
| 544 | ** Add a new tech note to the repository. The timestamp is |
| 545 | ** given by the zETime parameter. isNew must be true to create |
| 546 | ** a new page. If no previous page with the name zPageName exists |
| 547 | ** and isNew is false, then this routine throws an error. |
| 548 | */ |
| 549 | void event_cmd_commit( |
| 550 | char *zETime, /* timestamp */ |
| 551 | int isNew, /* true to create a new page */ |
| 552 | Blob *pContent, /* content of the new page */ |
| 553 | const char *zMimeType, /* mimetype of the content */ |
| 554 | const char *zComment, /* comment to go on the timeline */ |
| 555 | const char *zTags, /* tags */ |
| 556 | const char *zClr /* background color */ |
| 557 | ){ |
| 558 | int rid; /* Artifact id of the tech note */ |
| 559 | const char *zId; /* id of the tech note */ |
| 560 | rid = db_int(0, "SELECT objid FROM event" |
| 561 | " WHERE datetime(mtime)=datetime('%q') AND type = 'e'" |
| 562 | " LIMIT 1", |
| 563 | zETime |
| 564 | ); |
| 565 | if( rid==0 && !isNew ){ |
| 566 | #ifdef FOSSIL_ENABLE_JSON |
| 567 | g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND; |
| 568 | #endif |
| 569 | fossil_fatal("no such tech note: %s", zETime); |
| 570 | } |
| 571 | if( rid!=0 && isNew ){ |
| 572 | #ifdef FOSSIL_ENABLE_JSON |
| 573 | g.json.resultCode = FSL_JSON_E_RESOURCE_ALREADY_EXISTS; |
| 574 | #endif |
| 575 | fossil_fatal("tech note %s already exists", zETime); |
| 576 | } |
| 577 | |
| 578 | if ( isNew ){ |
| 579 | zId = db_text(0, "SELECT lower(hex(randomblob(20)))"); |
| 580 | }else{ |
| 581 | zId = db_text(0, |
| 582 | "SELECT substr(tagname,7) FROM tag" |
| 583 | " WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", |
| 584 |
| --- src/event.c | |
| +++ src/event.c | |
| @@ -540,44 +540,26 @@ | |
| 540 | style_footer(); |
| 541 | } |
| 542 | |
| 543 | /* |
| 544 | ** Add a new tech note to the repository. The timestamp is |
| 545 | ** given by the zETime parameter. rid must be zero to create |
| 546 | ** a new page. If no previous page with the name zPageName exists |
| 547 | ** and isNew is false, then this routine throws an error. |
| 548 | */ |
| 549 | void event_cmd_commit( |
| 550 | char *zETime, /* timestamp */ |
| 551 | int rid, /* Artifact id of the tech note */ |
| 552 | Blob *pContent, /* content of the new page */ |
| 553 | const char *zMimeType, /* mimetype of the content */ |
| 554 | const char *zComment, /* comment to go on the timeline */ |
| 555 | const char *zTags, /* tags */ |
| 556 | const char *zClr /* background color */ |
| 557 | ){ |
| 558 | const char *zId; /* id of the tech note */ |
| 559 | |
| 560 | if ( rid==0 ){ |
| 561 | zId = db_text(0, "SELECT lower(hex(randomblob(20)))"); |
| 562 | }else{ |
| 563 | zId = db_text(0, |
| 564 | "SELECT substr(tagname,7) FROM tag" |
| 565 | " WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", |
| 566 |
+30
-7
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -1330,10 +1330,11 @@ | ||
| 1330 | 1330 | const char *zDate = db_column_text(&q, 0); |
| 1331 | 1331 | const char *zUser = db_column_text(&q, 1); |
| 1332 | 1332 | const char *zCom = db_column_text(&q, 2); |
| 1333 | 1333 | const char *zType = db_column_text(&q, 3); |
| 1334 | 1334 | const char *zUuid = db_column_text(&q, 4); |
| 1335 | + int eventTagId = db_column_int(&q, 5); | |
| 1335 | 1336 | if( cnt>0 ){ |
| 1336 | 1337 | @ Also |
| 1337 | 1338 | } |
| 1338 | 1339 | if( zType[0]=='w' ){ |
| 1339 | 1340 | @ Wiki edit |
| @@ -1343,17 +1344,21 @@ | ||
| 1343 | 1344 | objType |= OBJTYPE_TICKET; |
| 1344 | 1345 | }else if( zType[0]=='c' ){ |
| 1345 | 1346 | @ Manifest of check-in |
| 1346 | 1347 | objType |= OBJTYPE_CHECKIN; |
| 1347 | 1348 | }else if( zType[0]=='e' ){ |
| 1348 | - @ Instance of technote | |
| 1349 | - objType |= OBJTYPE_EVENT; | |
| 1350 | - hyperlink_to_event_tagid(db_column_int(&q, 5)); | |
| 1349 | + if( eventTagId != 0) { | |
| 1350 | + @ Instance of technote | |
| 1351 | + objType |= OBJTYPE_EVENT; | |
| 1352 | + hyperlink_to_event_tagid(db_column_int(&q, 5)); | |
| 1353 | + }else{ | |
| 1354 | + @ Attachment to technote | |
| 1355 | + } | |
| 1351 | 1356 | }else{ |
| 1352 | 1357 | @ Tag referencing |
| 1353 | 1358 | } |
| 1354 | - if( zType[0]!='e' ){ | |
| 1359 | + if( zType[0]!='e' || eventTagId == 0){ | |
| 1355 | 1360 | hyperlink_to_uuid(zUuid); |
| 1356 | 1361 | } |
| 1357 | 1362 | @ - %!W(zCom) by |
| 1358 | 1363 | hyperlink_to_user(zUser,zDate," on"); |
| 1359 | 1364 | hyperlink_to_date(zDate, "."); |
| @@ -1383,14 +1388,32 @@ | ||
| 1383 | 1388 | }else{ |
| 1384 | 1389 | @ Attachment "%h(zFilename)" to |
| 1385 | 1390 | } |
| 1386 | 1391 | objType |= OBJTYPE_ATTACHMENT; |
| 1387 | 1392 | if( strlen(zTarget)==UUID_SIZE && validate16(zTarget,UUID_SIZE) ){ |
| 1388 | - if( g.perm.Hyperlink && g.anon.RdTkt ){ | |
| 1389 | - @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] | |
| 1393 | + if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", | |
| 1394 | + zTarget) | |
| 1395 | + ){ | |
| 1396 | + if( g.perm.Hyperlink && g.anon.RdTkt ){ | |
| 1397 | + @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] | |
| 1398 | + }else{ | |
| 1399 | + @ ticket [%S(zTarget)] | |
| 1400 | + } | |
| 1401 | + }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", | |
| 1402 | + zTarget) | |
| 1403 | + ){ | |
| 1404 | + if( g.perm.Hyperlink && g.anon.RdWiki ){ | |
| 1405 | + @ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)</a>] | |
| 1406 | + }else{ | |
| 1407 | + @ tech note [%S(zTarget)] | |
| 1408 | + } | |
| 1390 | 1409 | }else{ |
| 1391 | - @ ticket [%S(zTarget)] | |
| 1410 | + if( g.perm.Hyperlink && g.anon.RdWiki ){ | |
| 1411 | + @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] | |
| 1412 | + }else{ | |
| 1413 | + @ wiki page [%h(zTarget)] | |
| 1414 | + } | |
| 1392 | 1415 | } |
| 1393 | 1416 | }else{ |
| 1394 | 1417 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1395 | 1418 | @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
| 1396 | 1419 | }else{ |
| 1397 | 1420 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1330,10 +1330,11 @@ | |
| 1330 | const char *zDate = db_column_text(&q, 0); |
| 1331 | const char *zUser = db_column_text(&q, 1); |
| 1332 | const char *zCom = db_column_text(&q, 2); |
| 1333 | const char *zType = db_column_text(&q, 3); |
| 1334 | const char *zUuid = db_column_text(&q, 4); |
| 1335 | if( cnt>0 ){ |
| 1336 | @ Also |
| 1337 | } |
| 1338 | if( zType[0]=='w' ){ |
| 1339 | @ Wiki edit |
| @@ -1343,17 +1344,21 @@ | |
| 1343 | objType |= OBJTYPE_TICKET; |
| 1344 | }else if( zType[0]=='c' ){ |
| 1345 | @ Manifest of check-in |
| 1346 | objType |= OBJTYPE_CHECKIN; |
| 1347 | }else if( zType[0]=='e' ){ |
| 1348 | @ Instance of technote |
| 1349 | objType |= OBJTYPE_EVENT; |
| 1350 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1351 | }else{ |
| 1352 | @ Tag referencing |
| 1353 | } |
| 1354 | if( zType[0]!='e' ){ |
| 1355 | hyperlink_to_uuid(zUuid); |
| 1356 | } |
| 1357 | @ - %!W(zCom) by |
| 1358 | hyperlink_to_user(zUser,zDate," on"); |
| 1359 | hyperlink_to_date(zDate, "."); |
| @@ -1383,14 +1388,32 @@ | |
| 1383 | }else{ |
| 1384 | @ Attachment "%h(zFilename)" to |
| 1385 | } |
| 1386 | objType |= OBJTYPE_ATTACHMENT; |
| 1387 | if( strlen(zTarget)==UUID_SIZE && validate16(zTarget,UUID_SIZE) ){ |
| 1388 | if( g.perm.Hyperlink && g.anon.RdTkt ){ |
| 1389 | @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] |
| 1390 | }else{ |
| 1391 | @ ticket [%S(zTarget)] |
| 1392 | } |
| 1393 | }else{ |
| 1394 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1395 | @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
| 1396 | }else{ |
| 1397 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1330,10 +1330,11 @@ | |
| 1330 | const char *zDate = db_column_text(&q, 0); |
| 1331 | const char *zUser = db_column_text(&q, 1); |
| 1332 | const char *zCom = db_column_text(&q, 2); |
| 1333 | const char *zType = db_column_text(&q, 3); |
| 1334 | const char *zUuid = db_column_text(&q, 4); |
| 1335 | int eventTagId = db_column_int(&q, 5); |
| 1336 | if( cnt>0 ){ |
| 1337 | @ Also |
| 1338 | } |
| 1339 | if( zType[0]=='w' ){ |
| 1340 | @ Wiki edit |
| @@ -1343,17 +1344,21 @@ | |
| 1344 | objType |= OBJTYPE_TICKET; |
| 1345 | }else if( zType[0]=='c' ){ |
| 1346 | @ Manifest of check-in |
| 1347 | objType |= OBJTYPE_CHECKIN; |
| 1348 | }else if( zType[0]=='e' ){ |
| 1349 | if( eventTagId != 0) { |
| 1350 | @ Instance of technote |
| 1351 | objType |= OBJTYPE_EVENT; |
| 1352 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1353 | }else{ |
| 1354 | @ Attachment to technote |
| 1355 | } |
| 1356 | }else{ |
| 1357 | @ Tag referencing |
| 1358 | } |
| 1359 | if( zType[0]!='e' || eventTagId == 0){ |
| 1360 | hyperlink_to_uuid(zUuid); |
| 1361 | } |
| 1362 | @ - %!W(zCom) by |
| 1363 | hyperlink_to_user(zUser,zDate," on"); |
| 1364 | hyperlink_to_date(zDate, "."); |
| @@ -1383,14 +1388,32 @@ | |
| 1388 | }else{ |
| 1389 | @ Attachment "%h(zFilename)" to |
| 1390 | } |
| 1391 | objType |= OBJTYPE_ATTACHMENT; |
| 1392 | if( strlen(zTarget)==UUID_SIZE && validate16(zTarget,UUID_SIZE) ){ |
| 1393 | if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", |
| 1394 | zTarget) |
| 1395 | ){ |
| 1396 | if( g.perm.Hyperlink && g.anon.RdTkt ){ |
| 1397 | @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] |
| 1398 | }else{ |
| 1399 | @ ticket [%S(zTarget)] |
| 1400 | } |
| 1401 | }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", |
| 1402 | zTarget) |
| 1403 | ){ |
| 1404 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1405 | @ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)</a>] |
| 1406 | }else{ |
| 1407 | @ tech note [%S(zTarget)] |
| 1408 | } |
| 1409 | }else{ |
| 1410 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1411 | @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
| 1412 | }else{ |
| 1413 | @ wiki page [%h(zTarget)] |
| 1414 | } |
| 1415 | } |
| 1416 | }else{ |
| 1417 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1418 | @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
| 1419 | }else{ |
| 1420 |
+2
-1
| --- src/json_wiki.c | ||
| +++ src/json_wiki.c | ||
| @@ -374,12 +374,13 @@ | ||
| 374 | 374 | if(contentLen){ |
| 375 | 375 | blob_append(&content, cson_string_cstr(jstr),contentLen); |
| 376 | 376 | } |
| 377 | 377 | |
| 378 | 378 | zMimeType = json_find_option_cstr("mimetype","mimetype","M"); |
| 379 | + zMimeType = wiki_filter_mimetypes(zMimeType); | |
| 379 | 380 | |
| 380 | - wiki_cmd_commit(zPageName, 0==rid, &content, zMimeType, 0); | |
| 381 | + wiki_cmd_commit(zPageName, rid, &content, zMimeType, 0); | |
| 381 | 382 | blob_reset(&content); |
| 382 | 383 | /* |
| 383 | 384 | Our return value here has a race condition: if this operation |
| 384 | 385 | is called concurrently for the same wiki page via two requests, |
| 385 | 386 | payV could reflect the results of the other save operation. |
| 386 | 387 |
| --- src/json_wiki.c | |
| +++ src/json_wiki.c | |
| @@ -374,12 +374,13 @@ | |
| 374 | if(contentLen){ |
| 375 | blob_append(&content, cson_string_cstr(jstr),contentLen); |
| 376 | } |
| 377 | |
| 378 | zMimeType = json_find_option_cstr("mimetype","mimetype","M"); |
| 379 | |
| 380 | wiki_cmd_commit(zPageName, 0==rid, &content, zMimeType, 0); |
| 381 | blob_reset(&content); |
| 382 | /* |
| 383 | Our return value here has a race condition: if this operation |
| 384 | is called concurrently for the same wiki page via two requests, |
| 385 | payV could reflect the results of the other save operation. |
| 386 |
| --- src/json_wiki.c | |
| +++ src/json_wiki.c | |
| @@ -374,12 +374,13 @@ | |
| 374 | if(contentLen){ |
| 375 | blob_append(&content, cson_string_cstr(jstr),contentLen); |
| 376 | } |
| 377 | |
| 378 | zMimeType = json_find_option_cstr("mimetype","mimetype","M"); |
| 379 | zMimeType = wiki_filter_mimetypes(zMimeType); |
| 380 | |
| 381 | wiki_cmd_commit(zPageName, rid, &content, zMimeType, 0); |
| 382 | blob_reset(&content); |
| 383 | /* |
| 384 | Our return value here has a race condition: if this operation |
| 385 | is called concurrently for the same wiki page via two requests, |
| 386 | payV could reflect the results of the other save operation. |
| 387 |
+11
-6
| --- src/manifest.c | ||
| +++ src/manifest.c | ||
| @@ -2066,15 +2066,17 @@ | ||
| 2066 | 2066 | const char isAdd = (zSrc && zSrc[0]) ? 1 : 0; |
| 2067 | 2067 | char *zComment; |
| 2068 | 2068 | if( isAdd ){ |
| 2069 | 2069 | zComment = mprintf( |
| 2070 | 2070 | "Add attachment [/artifact/%!S|%h] to" |
| 2071 | - " tech note [/technote/%h|%.10h]", | |
| 2071 | + " tech note [/technote/%!S|%S]", | |
| 2072 | 2072 | zSrc, zName, zTarget, zTarget); |
| 2073 | 2073 | }else{ |
| 2074 | - zComment = mprintf("Delete attachment \"%h\" from tech note [%.10h]", | |
| 2075 | - zName, zTarget); | |
| 2074 | + zComment = mprintf( | |
| 2075 | + "Delete attachment \"%h\" from" | |
| 2076 | + " tech note [/technote/%!S|%S]", | |
| 2077 | + zName, zTarget, zTarget); | |
| 2076 | 2078 | } |
| 2077 | 2079 | db_multi_exec("UPDATE event SET comment=%Q, type='e'" |
| 2078 | 2080 | " WHERE objid=%Q", |
| 2079 | 2081 | zComment, zAttachId); |
| 2080 | 2082 | fossil_free(zComment); |
| @@ -2162,15 +2164,18 @@ | ||
| 2162 | 2164 | p->zAttachName, p->zAttachTarget); |
| 2163 | 2165 | } |
| 2164 | 2166 | }else if( 'e' == attachToType ){ |
| 2165 | 2167 | if( isAdd ){ |
| 2166 | 2168 | zComment = mprintf( |
| 2167 | - "Add attachment [/artifact/%!S|%h] to tech note [/technote/%h|%.10h]", | |
| 2169 | + "Add attachment [/artifact/%!S|%h] to tech note [/technote/%!S|%S]", | |
| 2168 | 2170 | p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget); |
| 2169 | 2171 | }else{ |
| 2170 | - zComment = mprintf("Delete attachment \"%h\" from tech note [%.10h]", | |
| 2171 | - p->zAttachName, p->zAttachTarget); | |
| 2172 | + zComment = mprintf( | |
| 2173 | + "Delete attachment \"/artifact/%!S|%h\" from" | |
| 2174 | + " tech note [/technote/%!S|%S]", | |
| 2175 | + p->zAttachName, p->zAttachName, | |
| 2176 | + p->zAttachTarget,p->zAttachTarget); | |
| 2172 | 2177 | } |
| 2173 | 2178 | }else{ |
| 2174 | 2179 | if( isAdd ){ |
| 2175 | 2180 | zComment = mprintf( |
| 2176 | 2181 | "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]", |
| 2177 | 2182 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -2066,15 +2066,17 @@ | |
| 2066 | const char isAdd = (zSrc && zSrc[0]) ? 1 : 0; |
| 2067 | char *zComment; |
| 2068 | if( isAdd ){ |
| 2069 | zComment = mprintf( |
| 2070 | "Add attachment [/artifact/%!S|%h] to" |
| 2071 | " tech note [/technote/%h|%.10h]", |
| 2072 | zSrc, zName, zTarget, zTarget); |
| 2073 | }else{ |
| 2074 | zComment = mprintf("Delete attachment \"%h\" from tech note [%.10h]", |
| 2075 | zName, zTarget); |
| 2076 | } |
| 2077 | db_multi_exec("UPDATE event SET comment=%Q, type='e'" |
| 2078 | " WHERE objid=%Q", |
| 2079 | zComment, zAttachId); |
| 2080 | fossil_free(zComment); |
| @@ -2162,15 +2164,18 @@ | |
| 2162 | p->zAttachName, p->zAttachTarget); |
| 2163 | } |
| 2164 | }else if( 'e' == attachToType ){ |
| 2165 | if( isAdd ){ |
| 2166 | zComment = mprintf( |
| 2167 | "Add attachment [/artifact/%!S|%h] to tech note [/technote/%h|%.10h]", |
| 2168 | p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget); |
| 2169 | }else{ |
| 2170 | zComment = mprintf("Delete attachment \"%h\" from tech note [%.10h]", |
| 2171 | p->zAttachName, p->zAttachTarget); |
| 2172 | } |
| 2173 | }else{ |
| 2174 | if( isAdd ){ |
| 2175 | zComment = mprintf( |
| 2176 | "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]", |
| 2177 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -2066,15 +2066,17 @@ | |
| 2066 | const char isAdd = (zSrc && zSrc[0]) ? 1 : 0; |
| 2067 | char *zComment; |
| 2068 | if( isAdd ){ |
| 2069 | zComment = mprintf( |
| 2070 | "Add attachment [/artifact/%!S|%h] to" |
| 2071 | " tech note [/technote/%!S|%S]", |
| 2072 | zSrc, zName, zTarget, zTarget); |
| 2073 | }else{ |
| 2074 | zComment = mprintf( |
| 2075 | "Delete attachment \"%h\" from" |
| 2076 | " tech note [/technote/%!S|%S]", |
| 2077 | zName, zTarget, zTarget); |
| 2078 | } |
| 2079 | db_multi_exec("UPDATE event SET comment=%Q, type='e'" |
| 2080 | " WHERE objid=%Q", |
| 2081 | zComment, zAttachId); |
| 2082 | fossil_free(zComment); |
| @@ -2162,15 +2164,18 @@ | |
| 2164 | p->zAttachName, p->zAttachTarget); |
| 2165 | } |
| 2166 | }else if( 'e' == attachToType ){ |
| 2167 | if( isAdd ){ |
| 2168 | zComment = mprintf( |
| 2169 | "Add attachment [/artifact/%!S|%h] to tech note [/technote/%!S|%S]", |
| 2170 | p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget); |
| 2171 | }else{ |
| 2172 | zComment = mprintf( |
| 2173 | "Delete attachment \"/artifact/%!S|%h\" from" |
| 2174 | " tech note [/technote/%!S|%S]", |
| 2175 | p->zAttachName, p->zAttachName, |
| 2176 | p->zAttachTarget,p->zAttachTarget); |
| 2177 | } |
| 2178 | }else{ |
| 2179 | if( isAdd ){ |
| 2180 | zComment = mprintf( |
| 2181 | "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]", |
| 2182 |
+162
-86
| --- src/wiki.c | ||
| +++ src/wiki.c | ||
| @@ -122,20 +122,35 @@ | ||
| 122 | 122 | static int is_sandbox(const char *zPagename){ |
| 123 | 123 | return fossil_stricmp(zPagename,"sandbox")==0 || |
| 124 | 124 | fossil_stricmp(zPagename,"sand box")==0; |
| 125 | 125 | } |
| 126 | 126 | |
| 127 | +/* | |
| 128 | +** Formal, common and short names for the various wiki styles. | |
| 129 | +*/ | |
| 130 | +static const char *const azStyles[] = { | |
| 131 | + "text/x-fossil-wiki", "Fossil Wiki", "wiki", | |
| 132 | + "text/x-markdown", "Markdown", "markdown", | |
| 133 | + "text/plain", "Plain Text", "plain" | |
| 134 | +}; | |
| 135 | + | |
| 127 | 136 | /* |
| 128 | 137 | ** Only allow certain mimetypes through. |
| 129 | 138 | ** All others become "text/x-fossil-wiki" |
| 130 | 139 | */ |
| 131 | 140 | const char *wiki_filter_mimetypes(const char *zMimetype){ |
| 132 | - if( zMimetype!=0 && | |
| 133 | - ( fossil_strcmp(zMimetype, "text/x-markdown")==0 | |
| 134 | - || fossil_strcmp(zMimetype, "text/plain")==0 ) | |
| 135 | - ){ | |
| 136 | - return zMimetype; | |
| 141 | + if( zMimetype!=0 ){ | |
| 142 | + int i; | |
| 143 | + for(i=0; i<sizeof(azStyles)/sizeof(azStyles[0]); i+=3){ | |
| 144 | + if( fossil_strcmp(zMimetype,azStyles[i+2])==0 ){ | |
| 145 | + return azStyles[i]; | |
| 146 | + } | |
| 147 | + } | |
| 148 | + if( fossil_strcmp(zMimetype, "text/x-markdown")==0 | |
| 149 | + || fossil_strcmp(zMimetype, "text/plain")==0 ){ | |
| 150 | + return zMimetype; | |
| 151 | + } | |
| 137 | 152 | } |
| 138 | 153 | return "text/x-fossil-wiki"; |
| 139 | 154 | } |
| 140 | 155 | |
| 141 | 156 | /* |
| @@ -412,27 +427,18 @@ | ||
| 412 | 427 | db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); |
| 413 | 428 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); |
| 414 | 429 | manifest_crosslink(nrid, pWiki, MC_NONE); |
| 415 | 430 | } |
| 416 | 431 | |
| 417 | -/* | |
| 418 | -** Formal names and common names for the various wiki styles. | |
| 419 | -*/ | |
| 420 | -static const char *const azStyles[] = { | |
| 421 | - "text/x-fossil-wiki", "Fossil Wiki", | |
| 422 | - "text/x-markdown", "Markdown", | |
| 423 | - "text/plain", "Plain Text" | |
| 424 | -}; | |
| 425 | - | |
| 426 | 432 | /* |
| 427 | 433 | ** Output a selection box from which the user can select the |
| 428 | 434 | ** wiki mimetype. |
| 429 | 435 | */ |
| 430 | 436 | void mimetype_option_menu(const char *zMimetype){ |
| 431 | 437 | unsigned i; |
| 432 | 438 | @ <select name="mimetype" size="1"> |
| 433 | - for(i=0; i<sizeof(azStyles)/sizeof(azStyles[0]); i+=2){ | |
| 439 | + for(i=0; i<sizeof(azStyles)/sizeof(azStyles[0]); i+=3){ | |
| 434 | 440 | if( fossil_strcmp(zMimetype,azStyles[i])==0 ){ |
| 435 | 441 | @ <option value="%s(azStyles[i])" selected>%s(azStyles[i+1])</option> |
| 436 | 442 | }else{ |
| 437 | 443 | @ <option value="%s(azStyles[i])">%s(azStyles[i+1])</option> |
| 438 | 444 | } |
| @@ -1068,47 +1074,26 @@ | ||
| 1068 | 1074 | style_footer(); |
| 1069 | 1075 | } |
| 1070 | 1076 | |
| 1071 | 1077 | /* |
| 1072 | 1078 | ** Add a new wiki page to the repository. The page name is |
| 1073 | -** given by the zPageName parameter. isNew must be true to create | |
| 1074 | -** a new page. If no previous page with the name zPageName exists | |
| 1075 | -** and isNew is false, then this routine throws an error. | |
| 1079 | +** given by the zPageName parameter. rid must be zero to create | |
| 1080 | +** a new page otherwise the page identified by rid is updated. | |
| 1076 | 1081 | ** |
| 1077 | 1082 | ** The content of the new page is given by the blob pContent. |
| 1078 | 1083 | ** |
| 1079 | 1084 | ** zMimeType specifies the N-card for the wiki page. If it is 0, |
| 1080 | 1085 | ** empty, or "text/x-fossil-wiki" (the default format) then it is |
| 1081 | 1086 | ** ignored. |
| 1082 | 1087 | */ |
| 1083 | -int wiki_cmd_commit(const char *zPageName, int isNew, Blob *pContent, | |
| 1088 | +int wiki_cmd_commit(const char *zPageName, int rid, Blob *pContent, | |
| 1084 | 1089 | const char *zMimeType, int localUser){ |
| 1085 | 1090 | Blob wiki; /* Wiki page content */ |
| 1086 | 1091 | Blob cksum; /* wiki checksum */ |
| 1087 | - int rid; /* artifact ID of parent page */ | |
| 1088 | 1092 | char *zDate; /* timestamp */ |
| 1089 | 1093 | char *zUuid; /* uuid for rid */ |
| 1090 | 1094 | |
| 1091 | - rid = db_int(0, | |
| 1092 | - "SELECT x.rid FROM tag t, tagxref x" | |
| 1093 | - " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" | |
| 1094 | - " ORDER BY x.mtime DESC LIMIT 1", | |
| 1095 | - zPageName | |
| 1096 | - ); | |
| 1097 | - if( rid==0 && !isNew ){ | |
| 1098 | -#ifdef FOSSIL_ENABLE_JSON | |
| 1099 | - g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND; | |
| 1100 | -#endif | |
| 1101 | - fossil_fatal("no such wiki page: %s", zPageName); | |
| 1102 | - } | |
| 1103 | - if( rid!=0 && isNew ){ | |
| 1104 | -#ifdef FOSSIL_ENABLE_JSON | |
| 1105 | - g.json.resultCode = FSL_JSON_E_RESOURCE_ALREADY_EXISTS; | |
| 1106 | -#endif | |
| 1107 | - fossil_fatal("wiki page %s already exists", zPageName); | |
| 1108 | - } | |
| 1109 | - | |
| 1110 | 1095 | blob_zero(&wiki); |
| 1111 | 1096 | zDate = date_in_standard_format("now"); |
| 1112 | 1097 | blob_appendf(&wiki, "D %s\n", zDate); |
| 1113 | 1098 | free(zDate); |
| 1114 | 1099 | blob_appendf(&wiki, "L %F\n", zPageName ); |
| @@ -1133,47 +1118,112 @@ | ||
| 1133 | 1118 | db_begin_transaction(); |
| 1134 | 1119 | wiki_put(&wiki, 0, wiki_need_moderation(localUser)); |
| 1135 | 1120 | db_end_transaction(0); |
| 1136 | 1121 | return 1; |
| 1137 | 1122 | } |
| 1123 | + | |
| 1124 | +/* | |
| 1125 | +** Determine the rid for a tech note given either its id or its | |
| 1126 | +** timestamp. Returns 0 if there is no such item and -1 if the details | |
| 1127 | +** are ambiguous and could refer to multiple items. | |
| 1128 | +*/ | |
| 1129 | +int wiki_technote_to_rid(const char *zETime) { | |
| 1130 | + int rid=0; /* Artifact ID of the tech note */ | |
| 1131 | + int nETime = strlen(zETime); | |
| 1132 | + Stmt q; | |
| 1133 | + if( nETime>=4 && nETime<=UUID_SIZE && validate16(zETime, nETime) ){ | |
| 1134 | + char zUuid[UUID_SIZE+1]; | |
| 1135 | + memcpy(zUuid, zETime, nETime+1); | |
| 1136 | + canonical16(zUuid, nETime); | |
| 1137 | + db_prepare(&q, | |
| 1138 | + "SELECT e.objid" | |
| 1139 | + " FROM event e, tag t" | |
| 1140 | + " WHERE e.type='e' AND e.tagid IS NOT NULL AND t.tagid=e.tagid" | |
| 1141 | + " AND t.tagname GLOB 'event-%q*'", | |
| 1142 | + zUuid | |
| 1143 | + ); | |
| 1144 | + if( db_step(&q)==SQLITE_ROW ){ | |
| 1145 | + rid = db_column_int(&q, 0); | |
| 1146 | + if( db_step(&q)==SQLITE_ROW ) rid = -1; | |
| 1147 | + } | |
| 1148 | + db_finalize(&q); | |
| 1149 | + } | |
| 1150 | + if (!rid) { | |
| 1151 | + if (strlen(zETime)>4) { | |
| 1152 | + rid = db_int(0, "SELECT objid" | |
| 1153 | + " FROM event" | |
| 1154 | + " WHERE datetime(mtime)=datetime('%q')" | |
| 1155 | + " AND type='e'" | |
| 1156 | + " AND tagid IS NOT NULL" | |
| 1157 | + " ORDER BY objid DESC LIMIT 1", | |
| 1158 | + zETime); | |
| 1159 | + } | |
| 1160 | + } | |
| 1161 | + return rid; | |
| 1162 | +} | |
| 1138 | 1163 | |
| 1139 | 1164 | /* |
| 1140 | 1165 | ** COMMAND: wiki* |
| 1141 | 1166 | ** |
| 1142 | 1167 | ** Usage: %fossil wiki (export|create|commit|list) WikiName |
| 1143 | 1168 | ** |
| 1144 | 1169 | ** Run various subcommands to work with wiki entries or tech notes. |
| 1145 | 1170 | ** |
| 1146 | -** %fossil wiki export ?PAGENAME? ?FILE? [-t|--technote DATETIME ] | |
| 1171 | +** %fossil wiki export PAGENAME ?FILE? | |
| 1172 | +** %fossil wiki export ?FILE? -t|--technote DATETIME|TECHNOTE-ID | |
| 1147 | 1173 | ** |
| 1148 | -** Sends the latest version of either the PAGENAME wiki entry | |
| 1149 | -** or the DATETIME tech note to the given file or standard | |
| 1150 | -** output. One of PAGENAME or DATETIME must be specified. | |
| 1174 | +** Sends the latest version of either a wiki page or of a tech note | |
| 1175 | +** to the given file or standard output. | |
| 1176 | +** If PAGENAME is provided, the wiki page will be output. For | |
| 1177 | +** a tech note either DATETIME or TECHNOTE-ID must be specified. If | |
| 1178 | +** DATETIME is used, the most recently modified tech note with that | |
| 1179 | +** DATETIME will be sent. | |
| 1151 | 1180 | ** |
| 1152 | 1181 | ** %fossil wiki (create|commit) PAGENAME ?FILE? ?OPTIONS? |
| 1153 | 1182 | ** |
| 1154 | 1183 | ** Create a new or commit changes to an existing wiki page or |
| 1155 | -** technote from FILE or from standard input. | |
| 1184 | +** technote from FILE or from standard input. PAGENAME is the | |
| 1185 | +** name of the wiki entry or the timeline comment of the | |
| 1186 | +** technote. | |
| 1156 | 1187 | ** |
| 1157 | 1188 | ** Options: |
| 1158 | -** -M|--mimetype TEXT-FORMAT The mimetype of the update defaulting | |
| 1159 | -** to the type used by the previous version | |
| 1160 | -** of the page or text/x-fossil-wiki. | |
| 1161 | -** -t|--technote DATETIME Specifies the timestamp of the technote | |
| 1162 | -** to be created or updated. | |
| 1189 | +** -M|--mimetype TEXT-FORMAT The mime type of the update. | |
| 1190 | +** Defaults to the type used by | |
| 1191 | +** the previous version of the | |
| 1192 | +** page, or text/x-fossil-wiki. | |
| 1193 | +** Valid values are: text/x-fossil-wiki, | |
| 1194 | +** text/markdown and text/plain. fossil, | |
| 1195 | +** markdown or plain can be specified as | |
| 1196 | +** synonyms of these values. | |
| 1197 | +** -t|--technote DATETIME Specifies the timestamp of | |
| 1198 | +** the technote to be created or | |
| 1199 | +** updated. When updating a tech note | |
| 1200 | +** the most recently modified tech note | |
| 1201 | +** with the specified timestamp will be | |
| 1202 | +** updated. | |
| 1203 | +** -t|--technote TECHNOTE-ID Specifies the technote to be | |
| 1204 | +** updated by its technote id. | |
| 1163 | 1205 | ** --technote-tags TAGS The set of tags for a technote. |
| 1164 | -** --technote-bgcolor COLOR The color used for the technote on the | |
| 1165 | -** timeline. | |
| 1206 | +** --technote-bgcolor COLOR The color used for the technote | |
| 1207 | +** on the timeline. | |
| 1166 | 1208 | ** |
| 1167 | -** %fossil wiki list ?--technote? | |
| 1168 | -** %fossil wiki ls ?--technote? | |
| 1209 | +** %fossil wiki list ?OPTIONS? | |
| 1210 | +** %fossil wiki ls ?OPTIONS? | |
| 1169 | 1211 | ** |
| 1170 | 1212 | ** Lists all wiki entries, one per line, ordered |
| 1171 | -** case-insensitively by name. The --technote flag | |
| 1172 | -** specifies that technotes will be listed instead of | |
| 1173 | -** the wiki entries, which will be listed in order | |
| 1174 | -** timestamp. | |
| 1213 | +** case-insensitively by name. | |
| 1214 | +** | |
| 1215 | +** Options: | |
| 1216 | +** -t|--technote Technotes will be listed instead of | |
| 1217 | +** pages. The technotes will be in order | |
| 1218 | +** of timestamp with the most recent | |
| 1219 | +** first. | |
| 1220 | +** -s|--show-technote-ids The id of the tech note will be listed | |
| 1221 | +** along side the timestamp. The tech note | |
| 1222 | +** id will be the first word on each line. | |
| 1223 | +** This option only applies if the | |
| 1224 | +** --technote option is also specified. | |
| 1175 | 1225 | ** |
| 1176 | 1226 | */ |
| 1177 | 1227 | void wiki_cmd(void){ |
| 1178 | 1228 | int n; |
| 1179 | 1229 | db_find_and_open_repository(0, 0); |
| @@ -1213,22 +1263,21 @@ | ||
| 1213 | 1263 | fossil_fatal("wiki page [%s] not found",zPageName); |
| 1214 | 1264 | } |
| 1215 | 1265 | zFile = (g.argc==4) ? "-" : g.argv[4]; |
| 1216 | 1266 | }else{ |
| 1217 | 1267 | if( (g.argc!=3) && (g.argc!=4) ){ |
| 1218 | - usage("export ?FILE? --technote DATETIME"); | |
| 1268 | + usage("export ?FILE? --technote DATETIME|TECHNOTE-ID"); | |
| 1219 | 1269 | } |
| 1220 | - rid = db_int(0, "SELECT objid FROM event" | |
| 1221 | - " WHERE datetime(mtime)=datetime('%q') AND type='e'" | |
| 1222 | - " ORDER BY mtime DESC LIMIT 1", | |
| 1223 | - zETime | |
| 1224 | - ); | |
| 1270 | + rid = wiki_technote_to_rid(zETime); | |
| 1271 | + if (rid == -1) { | |
| 1272 | + fossil_fatal("ambiguous tech note id: %s", zETime); | |
| 1273 | + } | |
| 1225 | 1274 | if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ |
| 1226 | 1275 | zBody = pWiki->zWiki; |
| 1227 | 1276 | } |
| 1228 | 1277 | if( zBody==0 ){ |
| 1229 | - fossil_fatal("technote not found"); | |
| 1278 | + fossil_fatal("technote [%s] not found",zETime); | |
| 1230 | 1279 | } |
| 1231 | 1280 | zFile = (g.argc==3) ? "-" : g.argv[3]; |
| 1232 | 1281 | } |
| 1233 | 1282 | for(i=strlen(zBody); i>0 && fossil_isspace(zBody[i-1]); i--){} |
| 1234 | 1283 | zBody[i] = 0; |
| @@ -1270,43 +1319,58 @@ | ||
| 1270 | 1319 | if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 |
| 1271 | 1320 | && (pWiki->zMimetype && *pWiki->zMimetype)){ |
| 1272 | 1321 | zMimeType = pWiki->zMimetype; |
| 1273 | 1322 | } |
| 1274 | 1323 | }else{ |
| 1275 | - rid = db_int(0, "SELECT objid FROM event" | |
| 1276 | - " WHERE datetime(mtime)=datetime('%q') AND type='e'" | |
| 1277 | - " ORDER BY mtime DESC LIMIT 1", | |
| 1278 | - zPageName | |
| 1279 | - ); | |
| 1324 | + rid = wiki_technote_to_rid(zETime); | |
| 1280 | 1325 | if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 |
| 1281 | 1326 | && (pWiki->zMimetype && *pWiki->zMimetype)){ |
| 1282 | 1327 | zMimeType = pWiki->zMimetype; |
| 1283 | 1328 | } |
| 1284 | 1329 | } |
| 1330 | + }else{ | |
| 1331 | + zMimeType = wiki_filter_mimetypes(zMimeType); | |
| 1332 | + } | |
| 1333 | + if( g.argv[2][1]=='r' && rid>0 ){ | |
| 1334 | + if ( !zETime ){ | |
| 1335 | + fossil_fatal("wiki page %s already exists", zPageName); | |
| 1336 | + }else{ | |
| 1337 | + /* Creating a tech note with same timestamp is permitted | |
| 1338 | + and should create a new tech note */ | |
| 1339 | + rid = 0; | |
| 1340 | + } | |
| 1341 | + }else if( g.argv[2][1]=='o' && rid == 0 ){ | |
| 1342 | + if ( !zETime ){ | |
| 1343 | + fossil_fatal("no such wiki page: %s", zPageName); | |
| 1344 | + }else{ | |
| 1345 | + fossil_fatal("no such tech note: %s", zETime); | |
| 1346 | + } | |
| 1285 | 1347 | } |
| 1348 | + | |
| 1286 | 1349 | if( !zETime ){ |
| 1350 | + wiki_cmd_commit(zPageName, rid, &content, zMimeType, 1); | |
| 1287 | 1351 | if( g.argv[2][1]=='r' ){ |
| 1288 | - wiki_cmd_commit(zPageName, 1, &content, zMimeType, 1); | |
| 1289 | 1352 | fossil_print("Created new wiki page %s.\n", zPageName); |
| 1290 | 1353 | }else{ |
| 1291 | - wiki_cmd_commit(zPageName, 0, &content, zMimeType, 1); | |
| 1292 | 1354 | fossil_print("Updated wiki page %s.\n", zPageName); |
| 1293 | 1355 | } |
| 1294 | 1356 | }else{ |
| 1295 | - char *zMETime; /* Normalized, mutable version of zETime */ | |
| 1296 | - zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", | |
| 1297 | - zETime); | |
| 1298 | - if( g.argv[2][1]=='r' ){ | |
| 1299 | - event_cmd_commit(zMETime, 1, &content, zMimeType, zPageName, | |
| 1300 | - zTags, zClr); | |
| 1301 | - fossil_print("Created new tech note %s.\n", zMETime); | |
| 1302 | - }else{ | |
| 1303 | - event_cmd_commit(zMETime, 0, &content, zMimeType, zPageName, | |
| 1304 | - zTags, zClr); | |
| 1305 | - fossil_print("Updated tech note %s.\n", zMETime); | |
| 1306 | - } | |
| 1307 | - free(zMETime); | |
| 1357 | + if( rid != -1 ){ | |
| 1358 | + char *zMETime; /* Normalized, mutable version of zETime */ | |
| 1359 | + zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", | |
| 1360 | + zETime); | |
| 1361 | + event_cmd_commit(zMETime, rid, &content, zMimeType, zPageName, | |
| 1362 | + zTags, zClr); | |
| 1363 | + if( g.argv[2][1]=='r' ){ | |
| 1364 | + fossil_print("Created new tech note %s.\n", zMETime); | |
| 1365 | + }else{ | |
| 1366 | + fossil_print("Updated tech note %s.\n", zMETime); | |
| 1367 | + } | |
| 1368 | + free(zMETime); | |
| 1369 | + }else{ | |
| 1370 | + fossil_fatal("ambiguous tech note id: %s", zETime); | |
| 1371 | + } | |
| 1308 | 1372 | } |
| 1309 | 1373 | manifest_destroy(pWiki); |
| 1310 | 1374 | blob_reset(&content); |
| 1311 | 1375 | }else if( strncmp(g.argv[2],"delete",n)==0 ){ |
| 1312 | 1376 | if( g.argc!=5 ){ |
| @@ -1314,23 +1378,35 @@ | ||
| 1314 | 1378 | } |
| 1315 | 1379 | fossil_fatal("delete not yet implemented."); |
| 1316 | 1380 | }else if(( strncmp(g.argv[2],"list",n)==0 ) |
| 1317 | 1381 | || ( strncmp(g.argv[2],"ls",n)==0 )){ |
| 1318 | 1382 | Stmt q; |
| 1383 | + int showIds = 0; | |
| 1384 | + | |
| 1319 | 1385 | if ( !find_option("technote","t",0) ){ |
| 1320 | 1386 | db_prepare(&q, |
| 1321 | 1387 | "SELECT substr(tagname, 6) FROM tag WHERE tagname GLOB 'wiki-*'" |
| 1322 | 1388 | " ORDER BY lower(tagname) /*sort*/" |
| 1323 | 1389 | ); |
| 1324 | 1390 | }else{ |
| 1391 | + showIds = find_option("show-technote-ids","s",0)!=0; | |
| 1325 | 1392 | db_prepare(&q, |
| 1326 | - "SELECT datetime(mtime) FROM event WHERE type='e'" | |
| 1327 | - " ORDER BY mtime /*sort*/" | |
| 1393 | + "SELECT datetime(e.mtime), substr(t.tagname,7)" | |
| 1394 | + " FROM event e, tag t" | |
| 1395 | + " WHERE e.type='e'" | |
| 1396 | + " AND e.tagid IS NOT NULL" | |
| 1397 | + " AND t.tagid=e.tagid" | |
| 1398 | + " ORDER BY e.mtime DESC /*sort*/" | |
| 1328 | 1399 | ); |
| 1329 | 1400 | } |
| 1401 | + | |
| 1330 | 1402 | while( db_step(&q)==SQLITE_ROW ){ |
| 1331 | 1403 | const char *zName = db_column_text(&q, 0); |
| 1404 | + if (showIds) { | |
| 1405 | + const char *zUuid = db_column_text(&q, 1); | |
| 1406 | + fossil_print("%s ",zUuid); | |
| 1407 | + } | |
| 1332 | 1408 | fossil_print( "%s\n",zName); |
| 1333 | 1409 | } |
| 1334 | 1410 | db_finalize(&q); |
| 1335 | 1411 | }else{ |
| 1336 | 1412 | goto wiki_cmd_usage; |
| 1337 | 1413 | |
| 1338 | 1414 | ADDED test/wiki.test |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -122,20 +122,35 @@ | |
| 122 | static int is_sandbox(const char *zPagename){ |
| 123 | return fossil_stricmp(zPagename,"sandbox")==0 || |
| 124 | fossil_stricmp(zPagename,"sand box")==0; |
| 125 | } |
| 126 | |
| 127 | /* |
| 128 | ** Only allow certain mimetypes through. |
| 129 | ** All others become "text/x-fossil-wiki" |
| 130 | */ |
| 131 | const char *wiki_filter_mimetypes(const char *zMimetype){ |
| 132 | if( zMimetype!=0 && |
| 133 | ( fossil_strcmp(zMimetype, "text/x-markdown")==0 |
| 134 | || fossil_strcmp(zMimetype, "text/plain")==0 ) |
| 135 | ){ |
| 136 | return zMimetype; |
| 137 | } |
| 138 | return "text/x-fossil-wiki"; |
| 139 | } |
| 140 | |
| 141 | /* |
| @@ -412,27 +427,18 @@ | |
| 412 | db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); |
| 413 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); |
| 414 | manifest_crosslink(nrid, pWiki, MC_NONE); |
| 415 | } |
| 416 | |
| 417 | /* |
| 418 | ** Formal names and common names for the various wiki styles. |
| 419 | */ |
| 420 | static const char *const azStyles[] = { |
| 421 | "text/x-fossil-wiki", "Fossil Wiki", |
| 422 | "text/x-markdown", "Markdown", |
| 423 | "text/plain", "Plain Text" |
| 424 | }; |
| 425 | |
| 426 | /* |
| 427 | ** Output a selection box from which the user can select the |
| 428 | ** wiki mimetype. |
| 429 | */ |
| 430 | void mimetype_option_menu(const char *zMimetype){ |
| 431 | unsigned i; |
| 432 | @ <select name="mimetype" size="1"> |
| 433 | for(i=0; i<sizeof(azStyles)/sizeof(azStyles[0]); i+=2){ |
| 434 | if( fossil_strcmp(zMimetype,azStyles[i])==0 ){ |
| 435 | @ <option value="%s(azStyles[i])" selected>%s(azStyles[i+1])</option> |
| 436 | }else{ |
| 437 | @ <option value="%s(azStyles[i])">%s(azStyles[i+1])</option> |
| 438 | } |
| @@ -1068,47 +1074,26 @@ | |
| 1068 | style_footer(); |
| 1069 | } |
| 1070 | |
| 1071 | /* |
| 1072 | ** Add a new wiki page to the repository. The page name is |
| 1073 | ** given by the zPageName parameter. isNew must be true to create |
| 1074 | ** a new page. If no previous page with the name zPageName exists |
| 1075 | ** and isNew is false, then this routine throws an error. |
| 1076 | ** |
| 1077 | ** The content of the new page is given by the blob pContent. |
| 1078 | ** |
| 1079 | ** zMimeType specifies the N-card for the wiki page. If it is 0, |
| 1080 | ** empty, or "text/x-fossil-wiki" (the default format) then it is |
| 1081 | ** ignored. |
| 1082 | */ |
| 1083 | int wiki_cmd_commit(const char *zPageName, int isNew, Blob *pContent, |
| 1084 | const char *zMimeType, int localUser){ |
| 1085 | Blob wiki; /* Wiki page content */ |
| 1086 | Blob cksum; /* wiki checksum */ |
| 1087 | int rid; /* artifact ID of parent page */ |
| 1088 | char *zDate; /* timestamp */ |
| 1089 | char *zUuid; /* uuid for rid */ |
| 1090 | |
| 1091 | rid = db_int(0, |
| 1092 | "SELECT x.rid FROM tag t, tagxref x" |
| 1093 | " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" |
| 1094 | " ORDER BY x.mtime DESC LIMIT 1", |
| 1095 | zPageName |
| 1096 | ); |
| 1097 | if( rid==0 && !isNew ){ |
| 1098 | #ifdef FOSSIL_ENABLE_JSON |
| 1099 | g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND; |
| 1100 | #endif |
| 1101 | fossil_fatal("no such wiki page: %s", zPageName); |
| 1102 | } |
| 1103 | if( rid!=0 && isNew ){ |
| 1104 | #ifdef FOSSIL_ENABLE_JSON |
| 1105 | g.json.resultCode = FSL_JSON_E_RESOURCE_ALREADY_EXISTS; |
| 1106 | #endif |
| 1107 | fossil_fatal("wiki page %s already exists", zPageName); |
| 1108 | } |
| 1109 | |
| 1110 | blob_zero(&wiki); |
| 1111 | zDate = date_in_standard_format("now"); |
| 1112 | blob_appendf(&wiki, "D %s\n", zDate); |
| 1113 | free(zDate); |
| 1114 | blob_appendf(&wiki, "L %F\n", zPageName ); |
| @@ -1133,47 +1118,112 @@ | |
| 1133 | db_begin_transaction(); |
| 1134 | wiki_put(&wiki, 0, wiki_need_moderation(localUser)); |
| 1135 | db_end_transaction(0); |
| 1136 | return 1; |
| 1137 | } |
| 1138 | |
| 1139 | /* |
| 1140 | ** COMMAND: wiki* |
| 1141 | ** |
| 1142 | ** Usage: %fossil wiki (export|create|commit|list) WikiName |
| 1143 | ** |
| 1144 | ** Run various subcommands to work with wiki entries or tech notes. |
| 1145 | ** |
| 1146 | ** %fossil wiki export ?PAGENAME? ?FILE? [-t|--technote DATETIME ] |
| 1147 | ** |
| 1148 | ** Sends the latest version of either the PAGENAME wiki entry |
| 1149 | ** or the DATETIME tech note to the given file or standard |
| 1150 | ** output. One of PAGENAME or DATETIME must be specified. |
| 1151 | ** |
| 1152 | ** %fossil wiki (create|commit) PAGENAME ?FILE? ?OPTIONS? |
| 1153 | ** |
| 1154 | ** Create a new or commit changes to an existing wiki page or |
| 1155 | ** technote from FILE or from standard input. |
| 1156 | ** |
| 1157 | ** Options: |
| 1158 | ** -M|--mimetype TEXT-FORMAT The mimetype of the update defaulting |
| 1159 | ** to the type used by the previous version |
| 1160 | ** of the page or text/x-fossil-wiki. |
| 1161 | ** -t|--technote DATETIME Specifies the timestamp of the technote |
| 1162 | ** to be created or updated. |
| 1163 | ** --technote-tags TAGS The set of tags for a technote. |
| 1164 | ** --technote-bgcolor COLOR The color used for the technote on the |
| 1165 | ** timeline. |
| 1166 | ** |
| 1167 | ** %fossil wiki list ?--technote? |
| 1168 | ** %fossil wiki ls ?--technote? |
| 1169 | ** |
| 1170 | ** Lists all wiki entries, one per line, ordered |
| 1171 | ** case-insensitively by name. The --technote flag |
| 1172 | ** specifies that technotes will be listed instead of |
| 1173 | ** the wiki entries, which will be listed in order |
| 1174 | ** timestamp. |
| 1175 | ** |
| 1176 | */ |
| 1177 | void wiki_cmd(void){ |
| 1178 | int n; |
| 1179 | db_find_and_open_repository(0, 0); |
| @@ -1213,22 +1263,21 @@ | |
| 1213 | fossil_fatal("wiki page [%s] not found",zPageName); |
| 1214 | } |
| 1215 | zFile = (g.argc==4) ? "-" : g.argv[4]; |
| 1216 | }else{ |
| 1217 | if( (g.argc!=3) && (g.argc!=4) ){ |
| 1218 | usage("export ?FILE? --technote DATETIME"); |
| 1219 | } |
| 1220 | rid = db_int(0, "SELECT objid FROM event" |
| 1221 | " WHERE datetime(mtime)=datetime('%q') AND type='e'" |
| 1222 | " ORDER BY mtime DESC LIMIT 1", |
| 1223 | zETime |
| 1224 | ); |
| 1225 | if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ |
| 1226 | zBody = pWiki->zWiki; |
| 1227 | } |
| 1228 | if( zBody==0 ){ |
| 1229 | fossil_fatal("technote not found"); |
| 1230 | } |
| 1231 | zFile = (g.argc==3) ? "-" : g.argv[3]; |
| 1232 | } |
| 1233 | for(i=strlen(zBody); i>0 && fossil_isspace(zBody[i-1]); i--){} |
| 1234 | zBody[i] = 0; |
| @@ -1270,43 +1319,58 @@ | |
| 1270 | if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 |
| 1271 | && (pWiki->zMimetype && *pWiki->zMimetype)){ |
| 1272 | zMimeType = pWiki->zMimetype; |
| 1273 | } |
| 1274 | }else{ |
| 1275 | rid = db_int(0, "SELECT objid FROM event" |
| 1276 | " WHERE datetime(mtime)=datetime('%q') AND type='e'" |
| 1277 | " ORDER BY mtime DESC LIMIT 1", |
| 1278 | zPageName |
| 1279 | ); |
| 1280 | if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 |
| 1281 | && (pWiki->zMimetype && *pWiki->zMimetype)){ |
| 1282 | zMimeType = pWiki->zMimetype; |
| 1283 | } |
| 1284 | } |
| 1285 | } |
| 1286 | if( !zETime ){ |
| 1287 | if( g.argv[2][1]=='r' ){ |
| 1288 | wiki_cmd_commit(zPageName, 1, &content, zMimeType, 1); |
| 1289 | fossil_print("Created new wiki page %s.\n", zPageName); |
| 1290 | }else{ |
| 1291 | wiki_cmd_commit(zPageName, 0, &content, zMimeType, 1); |
| 1292 | fossil_print("Updated wiki page %s.\n", zPageName); |
| 1293 | } |
| 1294 | }else{ |
| 1295 | char *zMETime; /* Normalized, mutable version of zETime */ |
| 1296 | zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", |
| 1297 | zETime); |
| 1298 | if( g.argv[2][1]=='r' ){ |
| 1299 | event_cmd_commit(zMETime, 1, &content, zMimeType, zPageName, |
| 1300 | zTags, zClr); |
| 1301 | fossil_print("Created new tech note %s.\n", zMETime); |
| 1302 | }else{ |
| 1303 | event_cmd_commit(zMETime, 0, &content, zMimeType, zPageName, |
| 1304 | zTags, zClr); |
| 1305 | fossil_print("Updated tech note %s.\n", zMETime); |
| 1306 | } |
| 1307 | free(zMETime); |
| 1308 | } |
| 1309 | manifest_destroy(pWiki); |
| 1310 | blob_reset(&content); |
| 1311 | }else if( strncmp(g.argv[2],"delete",n)==0 ){ |
| 1312 | if( g.argc!=5 ){ |
| @@ -1314,23 +1378,35 @@ | |
| 1314 | } |
| 1315 | fossil_fatal("delete not yet implemented."); |
| 1316 | }else if(( strncmp(g.argv[2],"list",n)==0 ) |
| 1317 | || ( strncmp(g.argv[2],"ls",n)==0 )){ |
| 1318 | Stmt q; |
| 1319 | if ( !find_option("technote","t",0) ){ |
| 1320 | db_prepare(&q, |
| 1321 | "SELECT substr(tagname, 6) FROM tag WHERE tagname GLOB 'wiki-*'" |
| 1322 | " ORDER BY lower(tagname) /*sort*/" |
| 1323 | ); |
| 1324 | }else{ |
| 1325 | db_prepare(&q, |
| 1326 | "SELECT datetime(mtime) FROM event WHERE type='e'" |
| 1327 | " ORDER BY mtime /*sort*/" |
| 1328 | ); |
| 1329 | } |
| 1330 | while( db_step(&q)==SQLITE_ROW ){ |
| 1331 | const char *zName = db_column_text(&q, 0); |
| 1332 | fossil_print( "%s\n",zName); |
| 1333 | } |
| 1334 | db_finalize(&q); |
| 1335 | }else{ |
| 1336 | goto wiki_cmd_usage; |
| 1337 | |
| 1338 | DDED test/wiki.test |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -122,20 +122,35 @@ | |
| 122 | static int is_sandbox(const char *zPagename){ |
| 123 | return fossil_stricmp(zPagename,"sandbox")==0 || |
| 124 | fossil_stricmp(zPagename,"sand box")==0; |
| 125 | } |
| 126 | |
| 127 | /* |
| 128 | ** Formal, common and short names for the various wiki styles. |
| 129 | */ |
| 130 | static const char *const azStyles[] = { |
| 131 | "text/x-fossil-wiki", "Fossil Wiki", "wiki", |
| 132 | "text/x-markdown", "Markdown", "markdown", |
| 133 | "text/plain", "Plain Text", "plain" |
| 134 | }; |
| 135 | |
| 136 | /* |
| 137 | ** Only allow certain mimetypes through. |
| 138 | ** All others become "text/x-fossil-wiki" |
| 139 | */ |
| 140 | const char *wiki_filter_mimetypes(const char *zMimetype){ |
| 141 | if( zMimetype!=0 ){ |
| 142 | int i; |
| 143 | for(i=0; i<sizeof(azStyles)/sizeof(azStyles[0]); i+=3){ |
| 144 | if( fossil_strcmp(zMimetype,azStyles[i+2])==0 ){ |
| 145 | return azStyles[i]; |
| 146 | } |
| 147 | } |
| 148 | if( fossil_strcmp(zMimetype, "text/x-markdown")==0 |
| 149 | || fossil_strcmp(zMimetype, "text/plain")==0 ){ |
| 150 | return zMimetype; |
| 151 | } |
| 152 | } |
| 153 | return "text/x-fossil-wiki"; |
| 154 | } |
| 155 | |
| 156 | /* |
| @@ -412,27 +427,18 @@ | |
| 427 | db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); |
| 428 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); |
| 429 | manifest_crosslink(nrid, pWiki, MC_NONE); |
| 430 | } |
| 431 | |
| 432 | /* |
| 433 | ** Output a selection box from which the user can select the |
| 434 | ** wiki mimetype. |
| 435 | */ |
| 436 | void mimetype_option_menu(const char *zMimetype){ |
| 437 | unsigned i; |
| 438 | @ <select name="mimetype" size="1"> |
| 439 | for(i=0; i<sizeof(azStyles)/sizeof(azStyles[0]); i+=3){ |
| 440 | if( fossil_strcmp(zMimetype,azStyles[i])==0 ){ |
| 441 | @ <option value="%s(azStyles[i])" selected>%s(azStyles[i+1])</option> |
| 442 | }else{ |
| 443 | @ <option value="%s(azStyles[i])">%s(azStyles[i+1])</option> |
| 444 | } |
| @@ -1068,47 +1074,26 @@ | |
| 1074 | style_footer(); |
| 1075 | } |
| 1076 | |
| 1077 | /* |
| 1078 | ** Add a new wiki page to the repository. The page name is |
| 1079 | ** given by the zPageName parameter. rid must be zero to create |
| 1080 | ** a new page otherwise the page identified by rid is updated. |
| 1081 | ** |
| 1082 | ** The content of the new page is given by the blob pContent. |
| 1083 | ** |
| 1084 | ** zMimeType specifies the N-card for the wiki page. If it is 0, |
| 1085 | ** empty, or "text/x-fossil-wiki" (the default format) then it is |
| 1086 | ** ignored. |
| 1087 | */ |
| 1088 | int wiki_cmd_commit(const char *zPageName, int rid, Blob *pContent, |
| 1089 | const char *zMimeType, int localUser){ |
| 1090 | Blob wiki; /* Wiki page content */ |
| 1091 | Blob cksum; /* wiki checksum */ |
| 1092 | char *zDate; /* timestamp */ |
| 1093 | char *zUuid; /* uuid for rid */ |
| 1094 | |
| 1095 | blob_zero(&wiki); |
| 1096 | zDate = date_in_standard_format("now"); |
| 1097 | blob_appendf(&wiki, "D %s\n", zDate); |
| 1098 | free(zDate); |
| 1099 | blob_appendf(&wiki, "L %F\n", zPageName ); |
| @@ -1133,47 +1118,112 @@ | |
| 1118 | db_begin_transaction(); |
| 1119 | wiki_put(&wiki, 0, wiki_need_moderation(localUser)); |
| 1120 | db_end_transaction(0); |
| 1121 | return 1; |
| 1122 | } |
| 1123 | |
| 1124 | /* |
| 1125 | ** Determine the rid for a tech note given either its id or its |
| 1126 | ** timestamp. Returns 0 if there is no such item and -1 if the details |
| 1127 | ** are ambiguous and could refer to multiple items. |
| 1128 | */ |
| 1129 | int wiki_technote_to_rid(const char *zETime) { |
| 1130 | int rid=0; /* Artifact ID of the tech note */ |
| 1131 | int nETime = strlen(zETime); |
| 1132 | Stmt q; |
| 1133 | if( nETime>=4 && nETime<=UUID_SIZE && validate16(zETime, nETime) ){ |
| 1134 | char zUuid[UUID_SIZE+1]; |
| 1135 | memcpy(zUuid, zETime, nETime+1); |
| 1136 | canonical16(zUuid, nETime); |
| 1137 | db_prepare(&q, |
| 1138 | "SELECT e.objid" |
| 1139 | " FROM event e, tag t" |
| 1140 | " WHERE e.type='e' AND e.tagid IS NOT NULL AND t.tagid=e.tagid" |
| 1141 | " AND t.tagname GLOB 'event-%q*'", |
| 1142 | zUuid |
| 1143 | ); |
| 1144 | if( db_step(&q)==SQLITE_ROW ){ |
| 1145 | rid = db_column_int(&q, 0); |
| 1146 | if( db_step(&q)==SQLITE_ROW ) rid = -1; |
| 1147 | } |
| 1148 | db_finalize(&q); |
| 1149 | } |
| 1150 | if (!rid) { |
| 1151 | if (strlen(zETime)>4) { |
| 1152 | rid = db_int(0, "SELECT objid" |
| 1153 | " FROM event" |
| 1154 | " WHERE datetime(mtime)=datetime('%q')" |
| 1155 | " AND type='e'" |
| 1156 | " AND tagid IS NOT NULL" |
| 1157 | " ORDER BY objid DESC LIMIT 1", |
| 1158 | zETime); |
| 1159 | } |
| 1160 | } |
| 1161 | return rid; |
| 1162 | } |
| 1163 | |
| 1164 | /* |
| 1165 | ** COMMAND: wiki* |
| 1166 | ** |
| 1167 | ** Usage: %fossil wiki (export|create|commit|list) WikiName |
| 1168 | ** |
| 1169 | ** Run various subcommands to work with wiki entries or tech notes. |
| 1170 | ** |
| 1171 | ** %fossil wiki export PAGENAME ?FILE? |
| 1172 | ** %fossil wiki export ?FILE? -t|--technote DATETIME|TECHNOTE-ID |
| 1173 | ** |
| 1174 | ** Sends the latest version of either a wiki page or of a tech note |
| 1175 | ** to the given file or standard output. |
| 1176 | ** If PAGENAME is provided, the wiki page will be output. For |
| 1177 | ** a tech note either DATETIME or TECHNOTE-ID must be specified. If |
| 1178 | ** DATETIME is used, the most recently modified tech note with that |
| 1179 | ** DATETIME will be sent. |
| 1180 | ** |
| 1181 | ** %fossil wiki (create|commit) PAGENAME ?FILE? ?OPTIONS? |
| 1182 | ** |
| 1183 | ** Create a new or commit changes to an existing wiki page or |
| 1184 | ** technote from FILE or from standard input. PAGENAME is the |
| 1185 | ** name of the wiki entry or the timeline comment of the |
| 1186 | ** technote. |
| 1187 | ** |
| 1188 | ** Options: |
| 1189 | ** -M|--mimetype TEXT-FORMAT The mime type of the update. |
| 1190 | ** Defaults to the type used by |
| 1191 | ** the previous version of the |
| 1192 | ** page, or text/x-fossil-wiki. |
| 1193 | ** Valid values are: text/x-fossil-wiki, |
| 1194 | ** text/markdown and text/plain. fossil, |
| 1195 | ** markdown or plain can be specified as |
| 1196 | ** synonyms of these values. |
| 1197 | ** -t|--technote DATETIME Specifies the timestamp of |
| 1198 | ** the technote to be created or |
| 1199 | ** updated. When updating a tech note |
| 1200 | ** the most recently modified tech note |
| 1201 | ** with the specified timestamp will be |
| 1202 | ** updated. |
| 1203 | ** -t|--technote TECHNOTE-ID Specifies the technote to be |
| 1204 | ** updated by its technote id. |
| 1205 | ** --technote-tags TAGS The set of tags for a technote. |
| 1206 | ** --technote-bgcolor COLOR The color used for the technote |
| 1207 | ** on the timeline. |
| 1208 | ** |
| 1209 | ** %fossil wiki list ?OPTIONS? |
| 1210 | ** %fossil wiki ls ?OPTIONS? |
| 1211 | ** |
| 1212 | ** Lists all wiki entries, one per line, ordered |
| 1213 | ** case-insensitively by name. |
| 1214 | ** |
| 1215 | ** Options: |
| 1216 | ** -t|--technote Technotes will be listed instead of |
| 1217 | ** pages. The technotes will be in order |
| 1218 | ** of timestamp with the most recent |
| 1219 | ** first. |
| 1220 | ** -s|--show-technote-ids The id of the tech note will be listed |
| 1221 | ** along side the timestamp. The tech note |
| 1222 | ** id will be the first word on each line. |
| 1223 | ** This option only applies if the |
| 1224 | ** --technote option is also specified. |
| 1225 | ** |
| 1226 | */ |
| 1227 | void wiki_cmd(void){ |
| 1228 | int n; |
| 1229 | db_find_and_open_repository(0, 0); |
| @@ -1213,22 +1263,21 @@ | |
| 1263 | fossil_fatal("wiki page [%s] not found",zPageName); |
| 1264 | } |
| 1265 | zFile = (g.argc==4) ? "-" : g.argv[4]; |
| 1266 | }else{ |
| 1267 | if( (g.argc!=3) && (g.argc!=4) ){ |
| 1268 | usage("export ?FILE? --technote DATETIME|TECHNOTE-ID"); |
| 1269 | } |
| 1270 | rid = wiki_technote_to_rid(zETime); |
| 1271 | if (rid == -1) { |
| 1272 | fossil_fatal("ambiguous tech note id: %s", zETime); |
| 1273 | } |
| 1274 | if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ |
| 1275 | zBody = pWiki->zWiki; |
| 1276 | } |
| 1277 | if( zBody==0 ){ |
| 1278 | fossil_fatal("technote [%s] not found",zETime); |
| 1279 | } |
| 1280 | zFile = (g.argc==3) ? "-" : g.argv[3]; |
| 1281 | } |
| 1282 | for(i=strlen(zBody); i>0 && fossil_isspace(zBody[i-1]); i--){} |
| 1283 | zBody[i] = 0; |
| @@ -1270,43 +1319,58 @@ | |
| 1319 | if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 |
| 1320 | && (pWiki->zMimetype && *pWiki->zMimetype)){ |
| 1321 | zMimeType = pWiki->zMimetype; |
| 1322 | } |
| 1323 | }else{ |
| 1324 | rid = wiki_technote_to_rid(zETime); |
| 1325 | if(rid>0 && (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 |
| 1326 | && (pWiki->zMimetype && *pWiki->zMimetype)){ |
| 1327 | zMimeType = pWiki->zMimetype; |
| 1328 | } |
| 1329 | } |
| 1330 | }else{ |
| 1331 | zMimeType = wiki_filter_mimetypes(zMimeType); |
| 1332 | } |
| 1333 | if( g.argv[2][1]=='r' && rid>0 ){ |
| 1334 | if ( !zETime ){ |
| 1335 | fossil_fatal("wiki page %s already exists", zPageName); |
| 1336 | }else{ |
| 1337 | /* Creating a tech note with same timestamp is permitted |
| 1338 | and should create a new tech note */ |
| 1339 | rid = 0; |
| 1340 | } |
| 1341 | }else if( g.argv[2][1]=='o' && rid == 0 ){ |
| 1342 | if ( !zETime ){ |
| 1343 | fossil_fatal("no such wiki page: %s", zPageName); |
| 1344 | }else{ |
| 1345 | fossil_fatal("no such tech note: %s", zETime); |
| 1346 | } |
| 1347 | } |
| 1348 | |
| 1349 | if( !zETime ){ |
| 1350 | wiki_cmd_commit(zPageName, rid, &content, zMimeType, 1); |
| 1351 | if( g.argv[2][1]=='r' ){ |
| 1352 | fossil_print("Created new wiki page %s.\n", zPageName); |
| 1353 | }else{ |
| 1354 | fossil_print("Updated wiki page %s.\n", zPageName); |
| 1355 | } |
| 1356 | }else{ |
| 1357 | if( rid != -1 ){ |
| 1358 | char *zMETime; /* Normalized, mutable version of zETime */ |
| 1359 | zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", |
| 1360 | zETime); |
| 1361 | event_cmd_commit(zMETime, rid, &content, zMimeType, zPageName, |
| 1362 | zTags, zClr); |
| 1363 | if( g.argv[2][1]=='r' ){ |
| 1364 | fossil_print("Created new tech note %s.\n", zMETime); |
| 1365 | }else{ |
| 1366 | fossil_print("Updated tech note %s.\n", zMETime); |
| 1367 | } |
| 1368 | free(zMETime); |
| 1369 | }else{ |
| 1370 | fossil_fatal("ambiguous tech note id: %s", zETime); |
| 1371 | } |
| 1372 | } |
| 1373 | manifest_destroy(pWiki); |
| 1374 | blob_reset(&content); |
| 1375 | }else if( strncmp(g.argv[2],"delete",n)==0 ){ |
| 1376 | if( g.argc!=5 ){ |
| @@ -1314,23 +1378,35 @@ | |
| 1378 | } |
| 1379 | fossil_fatal("delete not yet implemented."); |
| 1380 | }else if(( strncmp(g.argv[2],"list",n)==0 ) |
| 1381 | || ( strncmp(g.argv[2],"ls",n)==0 )){ |
| 1382 | Stmt q; |
| 1383 | int showIds = 0; |
| 1384 | |
| 1385 | if ( !find_option("technote","t",0) ){ |
| 1386 | db_prepare(&q, |
| 1387 | "SELECT substr(tagname, 6) FROM tag WHERE tagname GLOB 'wiki-*'" |
| 1388 | " ORDER BY lower(tagname) /*sort*/" |
| 1389 | ); |
| 1390 | }else{ |
| 1391 | showIds = find_option("show-technote-ids","s",0)!=0; |
| 1392 | db_prepare(&q, |
| 1393 | "SELECT datetime(e.mtime), substr(t.tagname,7)" |
| 1394 | " FROM event e, tag t" |
| 1395 | " WHERE e.type='e'" |
| 1396 | " AND e.tagid IS NOT NULL" |
| 1397 | " AND t.tagid=e.tagid" |
| 1398 | " ORDER BY e.mtime DESC /*sort*/" |
| 1399 | ); |
| 1400 | } |
| 1401 | |
| 1402 | while( db_step(&q)==SQLITE_ROW ){ |
| 1403 | const char *zName = db_column_text(&q, 0); |
| 1404 | if (showIds) { |
| 1405 | const char *zUuid = db_column_text(&q, 1); |
| 1406 | fossil_print("%s ",zUuid); |
| 1407 | } |
| 1408 | fossil_print( "%s\n",zName); |
| 1409 | } |
| 1410 | db_finalize(&q); |
| 1411 | }else{ |
| 1412 | goto wiki_cmd_usage; |
| 1413 | |
| 1414 | DDED test/wiki.test |
+334
| --- a/test/wiki.test | ||
| +++ b/test/wiki.test | ||
| @@ -0,0 +1,334 @@ | ||
| 1 | +# | |
| 2 | +# Copyright (c) 2016 D. Richard Hipp | |
| 3 | +# | |
| 4 | +# This program is free software; you can redistribute it and/or | |
| 5 | +# modify it under the terms of the Simplified BSD License (also | |
| 6 | +# known as the "2-Clause License" or "FreeBSD License".) | |
| 7 | +# | |
| 8 | +# This program is distributed in the hope that it will be useful, | |
| 9 | +# but without any warranty; without even the implied warranty of | |
| 10 | +# merchantability or fitness for a particular purpose. | |
| 11 | +# | |
| 12 | +# Author contact information: | |
| 13 | +# [email protected] | |
| 14 | +# http://www.hwaci.com/drh/ | |
| 15 | +# | |
| 16 | +############################################################################ | |
| 17 | +# | |
| 18 | +# Test wiki and attachment comman (c) 2016 D. Richard Hipp | |
| 19 | +# | |
| 20 | +# This program is free software; you can redistribute it and/or | |
| 21 | +# modify it under the terms of the Simplified BSD License (also | |
| 22 | +# known as the "2buted in the hoperegsub -all { +\n} $x \n x | |
| 23 | + (c) 2016 D. Richard Hipp | |
| 24 | +# | |
| 25 | +# # | |
| 26 | +# Copyright (c) 201 that it will be usefuy \n y | |
| 27 | + (c) 2016 D. Richard # | |
| 28 | +# Coi.com | |
| 29 | +# http://www.hwaci.com/drh/ | |
| 30 | +# | |
| 31 | +############################################################################ | |
| 32 | +# | |
| 33 | +# Test wiki and attachment command Support | |
| 34 | +# | |
| 35 | + | |
| 36 | +test_setup | |
| 37 | + | |
| 38 | +# Disable backoffice for this test, otherwise its process lingers for some | |
| 39 | +# time after the test has completed. | |
| 40 | +# Perhaps, this should be done in test_setup and enabled explicitly only | |
| 41 | +# when needed. | |
| 42 | +fossil set backoffice-disable 1 | |
| 43 | + | |
| 44 | +# Return true if two files are similar (i.e not only compress trailing spaces | |
| 45 | +# from a lset CODE [regex regsub -all { +\n} $x \n x | |
| 46 | + No info link found [read_file $b] | |
| 47 | + regsub -all { +http << "GET /artifact/$info D. Richard Hip# | |
| 48 | +# Copyright {a b} { | |
| 49 | + set x "" | |
| 50 | + if {[file exists $a]} { | |
| 51 | + set x [read_file $a] | |
| 52 | + regsub -all { +\n} $x \n x | |
| 53 | + regsub -all {\n$} $x {} x | |
| 54 | + } | |
| 55 | + set y "" | |
| 56 | + if {[file exists $b]} { | |
| 57 | + set y [read_file $b] | |
| 58 | + regsub -all { +\n} $y \n y | |
| 59 | + regsub -all {\n$} $y {} y | |
| 60 | + } | |
| 61 | + return [expr {$x==$y}] | |
| 62 | +} | |
| 63 | + | |
| 64 | +# Return the mime type in the manifest for a given wiki page | |
| 65 | +# Defaults to "error: some text" if the manifest can't be located and | |
| 66 | +# "text/x-fossil-wiki" (the default mimetype for rendering) | |
| 67 | +# if the N card is omitted in the manifest. | |
| 68 | +# Note: Makes fossil calls, so $CODE and $RESULT will be corrupted | |
| 69 | +proc get_mime_type {name} { | |
| 70 | + global CODE RESULT | |
| 71 | + fossil http << "GET /wiki?name=$name" | |
| 72 | + if {$CODE != 0} { | |
| 73 | + return "error: /wiki?name=$name $CODE $RESULT" | |
| 74 | + } | |
| 75 | + fossil whatis --type w $name | |
| 76 | + if {$CODE != 0} { | |
| 77 | + return "error: fossil whatis --type w $name $CODE $RESULT" | |
| 78 | + } | |
| 79 | + set CODE [regexp -line {^artifact:\s*([0-9a-f]+)$} $RESULT match info] | |
| 80 | + if {$CODE == 0} { | |
| 81 | + return "error: whatis returned no info for wiki page $name" | |
| 82 | + } | |
| 83 | + fossil artifact $info | |
| 84 | + if {$CODE != 0} { | |
| 85 | + return "error: fossil artifact $info $CODE $RESULT" | |
| 86 | + } | |
| 87 | + set CODE [regexp -line {^N (.*)$} $RESULT match mimetype] | |
| 88 | + if {$CODE == 0} { | |
| 89 | + return "text/x-fossil-wiki" | |
| 90 | + } | |
| 91 | + return $mimetype | |
| 92 | +} | |
| 93 | + | |
| 94 | + | |
| 95 | +############################################################################### | |
| 96 | +# Initially there should be no wiki entries | |
| 97 | +fossil wiki list | |
| 98 | +test wiki-0 {[normalize_result] eq {}} | |
| 99 | + | |
| 100 | +############################################################################### | |
| 101 | +# Adding an entry should add it to the wiki list | |
| 102 | +write_file f1 "first wiki note" | |
| 103 | +fossil wiki create tcltest f1 | |
| 104 | +test wiki-1 {$CODE == 0} | |
| 105 | +fossil wiki list | |
| 106 | +test wiki-2 {[normalize_result] eq {tcltest}} | |
| 107 | + | |
| 108 | +############################################################################### | |
| 109 | +# Trying to add the same entry should fail | |
| 110 | +fossil wiki create tcltest f1 -expectError | |
| 111 | +test wiki-3 {$CODE != 0} | |
| 112 | + | |
| 113 | +############################################################################### | |
| 114 | +# exporting the wiki page should give back similar text | |
| 115 | +fossil wiki export tcltest a1 | |
| 116 | +test wiki-4 {[similar_file f1 a1]} | |
| 117 | + | |
| 118 | +############################################################################### | |
| 119 | +# commiting a change to an existing page should replace the page on export | |
| 120 | +write_file f2 "second version of the page" | |
| 121 | +fossil wiki commit tcltest f2 | |
| 122 | +test wiki-5 {$CODE == 0} | |
| 123 | +fossil wiki export tcltest a2 | |
| 124 | +test wiki-6 {[similar_file f2 a2]} | |
| 125 | + | |
| 126 | +############################################################################### | |
| 127 | +# But we shouldn't be able to update non-existant pages | |
| 128 | +fossil wiki commit doesntexist f1 -expectError | |
| 129 | +test wiki-7 {$CODE != 0} | |
| 130 | + | |
| 131 | +############################################################################### | |
| 132 | +# There shouldn't be any tech notes at this point | |
| 133 | +fossil wiki list --technote | |
| 134 | +test wiki-8 {[normalize_result] eq {}} | |
| 135 | + | |
| 136 | +############################################################################### | |
| 137 | +# Creating a tech note with a specified timestamp should add a technote | |
| 138 | +write_file f3 "A technote" | |
| 139 | +f ossil wiki create technote f3 --technote {2016-01-01 12:34} | |
| 140 | +test wiki-9 {$CODE == 0} | |
| 141 | +fossil wiki list --technote | |
| 142 | +test wiki-10 {[normalize_result] eq {2016-01-01 12:34:00}} | |
| 143 | +fossil wiki list --technote --show-technote-ids | |
| 144 | +set technotelist [split $RESULT "\n"] | |
| 145 | +set veryfirsttechnoteid [lindex [split [lindex $technotelist 0]] 0] | |
| 146 | + | |
| 147 | +############################################################################### | |
| 148 | +# exporting that technote should give back similar text | |
| 149 | +fossil wiki export a3 --technote {2016-01-01 12:34:00} | |
| 150 | +test wiki-11 {[similar_file f3 a3]} | |
| 151 | + | |
| 152 | +############################################################################### | |
| 153 | +# Trying to add a technote with the same timestamp should succeed and create a | |
| 154 | +# second tech note | |
| 155 | +fossil wiki create 2ndnote f3 -technote {2016-01-01 12:34} | |
| 156 | +test wiki-13 {$CODE == 0} | |
| 157 | +fossil wiki list --technote | |
| 158 | +set technotelist [split $RESULT "\n"] | |
| 159 | +test wiki-13.1 {[llength $technotelist] == 2} | |
| 160 | + | |
| 161 | +############################################################################### | |
| 162 | +# commiting a change to an existing technote should replace the page on export | |
| 163 | +# (this should update th rt | |
| 164 | +# (this should update the tech note from wiki-13 as that the most recently | |
| 165 | +# updated one, that should also be the one exported by the export command) | |
| 166 | +write_file f4 "technote 2nd variant" | |
| 167 | +fossil wiki commit technote f4 --technote {2016-01-01 12:34} | |
| 168 | +test wiki-14 {$CODE == 0} | |
| 169 | +fossil wiki export a4 --technote {2016-01-01 12:34} | |
| 170 | +test wiki-15 {[similar_file f4 a4]} | |
| 171 | +# Also check that the tech note with the same timestamp, but modified less | |
| 172 | +# recently still has its original text | |
| 173 | +fossil wiki export a4.1 --technote $veryfirsttechnoteid | |
| 174 | +test wiki-15.1 {[similar_file f3 a4.1]} | |
| 175 | + | |
| 176 | +############################################################################### | |
| 177 | +# But we shouldn't be able to update non-existant pages | |
| 178 | +fossil wiki commit doesntexist f1 -expectError | |
| 179 | +test wiki-16 {$CODE != 0} | |
| 180 | + | |
| 181 | +############################################################################### | |
| 182 | +# Check specifying tags for a technote is OK | |
| 183 | +write_file f5 "technote with tags" | |
| 184 | +fossil wiki create {tagged technote} f5 --technote {2016-01-02 12:34} --technote-tags {A B} | |
| 185 | +test wiki-17 {$CODE == 0} | |
| 186 | +write_file f5.1 "editted and tagged technote" | |
| 187 | +fossil wiki commit {tagged technote} f5 --technote {2016-01-02 12:34} --t note {2016-01 -03 12:34} --technote-bgcolor blue | |
| 188 | +test wiki-28 {$CODE == 0} | |
| 189 | + | |
| 190 | +############################################################################### | |
| 191 | +# _file f7 "Different timestamps" | |
| 192 | +fossil wiki create technotenow f7 --technote {2016-01-04 12:34:56+00:00} | |
| 193 | +test wiki-29 {$CODE == 0} | |
| 194 | + | |
| 195 | +############################################################################### | |
| 196 | +# Check a technote appears on the timeline | |
| 197 | +write_file f8 "Contents of a 'unique' tech note" | |
| 198 | +fossil wiki create {Unique technote} f8 --technote {2016-01-05 01:02:03} | |
| 199 | +fossil timeline | |
| 200 | +test wiki-30 {[string match *Unique*technote* $RESULT]} | |
| 201 | + | |
| 202 | +############################################################################### | |
| 203 | +# Check for a collision between an attachment and a note, this was a | |
| 204 | +# bug that resulted from some code treating the attachment entry as if it | |
| 205 | +# were a technote when it isn't really. | |
| 206 | +# | |
| 207 | +# First, wait for the top of the next second so the attachment | |
| 208 | +# happens at a known time, then add an attachment to an existing note | |
| 209 | +# and a new note immediately after. | |
| 210 | + | |
| 211 | +set t0 [clock seconds] | |
| 212 | +while {$t0 == [clock seconds]} { | |
| 213 | + after 100 | |
| 214 | +} | |
| 215 | +set t1 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] | |
| 216 | +write_fil -%m-%d %H:%M:%S"] | |
| 217 | +write_file f9 "Timestamp: $t1" | |
| 218 | +fossil attachment add f9 --technote {2016-01-05 01:02:03} | |
| 219 | +test wiki-31 {$CODE == 0} | |
| 220 | +fossil wiki create {A CODE == 0} | |
| 221 | +# | |
| 222 | +# Now waste time until the next second so that the remaining tests | |
| 223 | +# don't have to worry about a potential collision | |
| 224 | +set t0 [clock seconds] | |
| 225 | +while {$t0 == [clock seconds]} { | |
| 226 | + after 100 | |
| 227 | +} | |
| 228 | + | |
| 229 | +############################################################################### | |
| 230 | +# Check a technote with no timestamp cannot be created, but that | |
| 231 | +# "now " is a valid stamp. | |
| 232 | +set t2 [clock format [clock seconds] -gmt 1 -format # Copyright (c) 2016 D. Richard Hiiotelist [llength $technotelified timest ki create technotenow f7 --technote {2016-01-04 12:34:56+00:00} | |
| 233 | +test wiki-29 {$CODE == 0} | |
| 234 | + | |
| 235 | +############################################################################### | |
| 236 | +# Check a technote appears on the timeline | |
| 237 | +write_file f8 "Contents of a 'unique' tech note" | |
| 238 | +fossil wiki create {Unique technote} f8 --technote {2016-01-05 01:02:03} | |
| 239 | +fossil timeline | |
| 240 | +test wiki-30 {[string match *Unique*technote* $RESULT]} | |
| 241 | + | |
| 242 | +############################################################################### | |
| 243 | +# Check for a collision between an attachment and a note, this was a | |
| 244 | +# bug that resulted from some code treating the attachment entry as if it | |
| 245 | +# were a technote when it isn't really. | |
| 246 | +# | |
| 247 | +# First, wait for the top of the next second so the attachment | |
| 248 | +# happens at a known time, then add an attachment to an existing note | |
| 249 | +# and a new note immediately after. | |
| 250 | + | |
| 251 | +set t0 [clock seconds] | |
| 252 | +while {$t0 == [clock seconds]} { | |
| 253 | + after 100 | |
| 254 | +} | |
| 255 | +set t1 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] | |
| 256 | +write_file f9 "Timestamp: $t1" | |
| 257 | +fossil attachment add f9 --technote {2016-01-05 01:02:03} | |
| 258 | +test wiki-31 {$CODE == 0} | |
| 259 | +fossil wiki create {Attachment collision} f9 --technote now | |
| 260 | +test wiki-32 {$CODE == 0} | |
| 261 | +# | |
| 262 | +# Now waste time until the next second so that the remaining tests | |
| 263 | +# don't have to worry about a potential collision | |
| 264 | +set t0 [clock seconds] | |
| 265 | +while {$t0 == [clock seconds]} { | |
| 266 | + after 100 | |
| 267 | +} | |
| 268 | + | |
| 269 | +############################################################################### | |
| 270 | +# Check a technote with no timestamp cannot be created, but that | |
| 271 | +# "now" is a valid stamp. | |
| 272 | +s et t2 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] | |
| 273 | +write_file f10 "Even unstampted notes are delivered.\nStamped $t2" | |
| 274 | +fossil wiki create "Unstamped Note" f10 --technote -expectError | |
| 275 | +test wiki-33 {$CODE != 0} | |
| 276 | +fossil wiki create "Unstamped Note" f10 --technote now | |
| 277 | +test wiki-34 {$CODE == 0} | |
| 278 | +fossil wiki list -t | |
| 279 | +test wiki-35 {[string match "*$t2*" $RESULT]} | |
| 280 | + | |
| 281 | +############################################################################### | |
| 282 | +# Check an attachment to it in the same second works. | |
| 283 | +write_file f11 "Time Stamp was $t2" | |
| 284 | +fossil attachment add f11 --technote $t2 | |
| 285 | +test wiki-36 {$CODE == 0} | |
| 286 | +fossil timeline | |
| 287 | +test wiki-36-1 {$CODE == 0} | |
| 288 | +fossil wiki list -t | |
| 289 | +test wiki-36-2 {$CODE == 0} | |
| 290 | + | |
| 291 | +########################################################################### #technotelist [split $RESULT "\n"] | |
| 292 | +for {set i 0} {$i < [llength $technotelist]} {incr i} { | |
| 293 | + set fullid [lindex $technotelist $i] | |
| 294 | + set id [string range $fullid 0 3] | |
| 295 | + dict incr idcounts $id | |
| 296 | + if {[dict get $idcounts $id] > $maxcount} { | |
| 297 | + set maxid $id | |
| 298 | + incr maxcount | |
| 299 | + } | |
| 300 | +} | |
| 301 | +# get i so that, as a julian date, it is in the 1800s, i.e., older than | |
| 302 | +# any other tech note, but after 1 AD | |
| 303 | +set i 2400000 | |
| 304 | +while {$maxcount < 2} { | |
| 305 | + # keep getting older | |
| 306 | + incr i -1 | |
| 307 | + write_file f13 "A tech note with timestamp of jday=$i" | |
| 308 | + fossil wiki create "timestamp of $i" f13 --technote "$i" | |
| 309 | + fossil wiki list --technote --show-technote-ids | |
| 310 | + set technotelist [split $RESULT "\n"] | |
| 311 | + set oldesttechnoteid [lindex [split [lindex $technotelist [llength $technotelist]-1]] 0] | |
| 312 | + set id [string range $oldesttechnoteid 0 3] | |
| 313 | + dict incr idcounts $id | |
| 314 | + if {[dict get $idcounts $id] > $maxcount} { | |
| 315 | + set maxid $id | |
| 316 | + incr maxcount | |
| 317 | + } | |
| 318 | +} | |
| 319 | +# Save the duplicate id for this and later tests | |
| 320 | +set duplicateid $maxid | |
| 321 | +fossil wiki export a13 --technote $duplicateid -expectError | |
| 322 | +test wiki-42 {$CODE != 0} | |
| 323 | + | |
| 324 | +############################################################################### | |
| 325 | +# Check we can update technote by its id | |
| 326 | +write_file f14 "Updated text for the really old tech note" | |
| 327 | +fossil wiki commit {Old tech note} f14 --technote $anoldtechnoteid | |
| 328 | +fossil wiki export a14 --technote $anoldtechnoteid | |
| 329 | +test wiki-43 {[similar_file f14 a14]} | |
| 330 | + | |
| 331 | +############################################################################### | |
| 332 | +# Check we can add attachments to a technote by its id | |
| 333 | +fossil attachment add fa --technote $anoldtechnoteid | |
| 334 | +test |
| --- a/test/wiki.test | |
| +++ b/test/wiki.test | |
| @@ -0,0 +1,334 @@ | |
| --- a/test/wiki.test | |
| +++ b/test/wiki.test | |
| @@ -0,0 +1,334 @@ | |
| 1 | # |
| 2 | # Copyright (c) 2016 D. Richard Hipp |
| 3 | # |
| 4 | # This program is free software; you can redistribute it and/or |
| 5 | # modify it under the terms of the Simplified BSD License (also |
| 6 | # known as the "2-Clause License" or "FreeBSD License".) |
| 7 | # |
| 8 | # This program is distributed in the hope that it will be useful, |
| 9 | # but without any warranty; without even the implied warranty of |
| 10 | # merchantability or fitness for a particular purpose. |
| 11 | # |
| 12 | # Author contact information: |
| 13 | # [email protected] |
| 14 | # http://www.hwaci.com/drh/ |
| 15 | # |
| 16 | ############################################################################ |
| 17 | # |
| 18 | # Test wiki and attachment comman (c) 2016 D. Richard Hipp |
| 19 | # |
| 20 | # This program is free software; you can redistribute it and/or |
| 21 | # modify it under the terms of the Simplified BSD License (also |
| 22 | # known as the "2buted in the hoperegsub -all { +\n} $x \n x |
| 23 | (c) 2016 D. Richard Hipp |
| 24 | # |
| 25 | # # |
| 26 | # Copyright (c) 201 that it will be usefuy \n y |
| 27 | (c) 2016 D. Richard # |
| 28 | # Coi.com |
| 29 | # http://www.hwaci.com/drh/ |
| 30 | # |
| 31 | ############################################################################ |
| 32 | # |
| 33 | # Test wiki and attachment command Support |
| 34 | # |
| 35 | |
| 36 | test_setup |
| 37 | |
| 38 | # Disable backoffice for this test, otherwise its process lingers for some |
| 39 | # time after the test has completed. |
| 40 | # Perhaps, this should be done in test_setup and enabled explicitly only |
| 41 | # when needed. |
| 42 | fossil set backoffice-disable 1 |
| 43 | |
| 44 | # Return true if two files are similar (i.e not only compress trailing spaces |
| 45 | # from a lset CODE [regex regsub -all { +\n} $x \n x |
| 46 | No info link found [read_file $b] |
| 47 | regsub -all { +http << "GET /artifact/$info D. Richard Hip# |
| 48 | # Copyright {a b} { |
| 49 | set x "" |
| 50 | if {[file exists $a]} { |
| 51 | set x [read_file $a] |
| 52 | regsub -all { +\n} $x \n x |
| 53 | regsub -all {\n$} $x {} x |
| 54 | } |
| 55 | set y "" |
| 56 | if {[file exists $b]} { |
| 57 | set y [read_file $b] |
| 58 | regsub -all { +\n} $y \n y |
| 59 | regsub -all {\n$} $y {} y |
| 60 | } |
| 61 | return [expr {$x==$y}] |
| 62 | } |
| 63 | |
| 64 | # Return the mime type in the manifest for a given wiki page |
| 65 | # Defaults to "error: some text" if the manifest can't be located and |
| 66 | # "text/x-fossil-wiki" (the default mimetype for rendering) |
| 67 | # if the N card is omitted in the manifest. |
| 68 | # Note: Makes fossil calls, so $CODE and $RESULT will be corrupted |
| 69 | proc get_mime_type {name} { |
| 70 | global CODE RESULT |
| 71 | fossil http << "GET /wiki?name=$name" |
| 72 | if {$CODE != 0} { |
| 73 | return "error: /wiki?name=$name $CODE $RESULT" |
| 74 | } |
| 75 | fossil whatis --type w $name |
| 76 | if {$CODE != 0} { |
| 77 | return "error: fossil whatis --type w $name $CODE $RESULT" |
| 78 | } |
| 79 | set CODE [regexp -line {^artifact:\s*([0-9a-f]+)$} $RESULT match info] |
| 80 | if {$CODE == 0} { |
| 81 | return "error: whatis returned no info for wiki page $name" |
| 82 | } |
| 83 | fossil artifact $info |
| 84 | if {$CODE != 0} { |
| 85 | return "error: fossil artifact $info $CODE $RESULT" |
| 86 | } |
| 87 | set CODE [regexp -line {^N (.*)$} $RESULT match mimetype] |
| 88 | if {$CODE == 0} { |
| 89 | return "text/x-fossil-wiki" |
| 90 | } |
| 91 | return $mimetype |
| 92 | } |
| 93 | |
| 94 | |
| 95 | ############################################################################### |
| 96 | # Initially there should be no wiki entries |
| 97 | fossil wiki list |
| 98 | test wiki-0 {[normalize_result] eq {}} |
| 99 | |
| 100 | ############################################################################### |
| 101 | # Adding an entry should add it to the wiki list |
| 102 | write_file f1 "first wiki note" |
| 103 | fossil wiki create tcltest f1 |
| 104 | test wiki-1 {$CODE == 0} |
| 105 | fossil wiki list |
| 106 | test wiki-2 {[normalize_result] eq {tcltest}} |
| 107 | |
| 108 | ############################################################################### |
| 109 | # Trying to add the same entry should fail |
| 110 | fossil wiki create tcltest f1 -expectError |
| 111 | test wiki-3 {$CODE != 0} |
| 112 | |
| 113 | ############################################################################### |
| 114 | # exporting the wiki page should give back similar text |
| 115 | fossil wiki export tcltest a1 |
| 116 | test wiki-4 {[similar_file f1 a1]} |
| 117 | |
| 118 | ############################################################################### |
| 119 | # commiting a change to an existing page should replace the page on export |
| 120 | write_file f2 "second version of the page" |
| 121 | fossil wiki commit tcltest f2 |
| 122 | test wiki-5 {$CODE == 0} |
| 123 | fossil wiki export tcltest a2 |
| 124 | test wiki-6 {[similar_file f2 a2]} |
| 125 | |
| 126 | ############################################################################### |
| 127 | # But we shouldn't be able to update non-existant pages |
| 128 | fossil wiki commit doesntexist f1 -expectError |
| 129 | test wiki-7 {$CODE != 0} |
| 130 | |
| 131 | ############################################################################### |
| 132 | # There shouldn't be any tech notes at this point |
| 133 | fossil wiki list --technote |
| 134 | test wiki-8 {[normalize_result] eq {}} |
| 135 | |
| 136 | ############################################################################### |
| 137 | # Creating a tech note with a specified timestamp should add a technote |
| 138 | write_file f3 "A technote" |
| 139 | f ossil wiki create technote f3 --technote {2016-01-01 12:34} |
| 140 | test wiki-9 {$CODE == 0} |
| 141 | fossil wiki list --technote |
| 142 | test wiki-10 {[normalize_result] eq {2016-01-01 12:34:00}} |
| 143 | fossil wiki list --technote --show-technote-ids |
| 144 | set technotelist [split $RESULT "\n"] |
| 145 | set veryfirsttechnoteid [lindex [split [lindex $technotelist 0]] 0] |
| 146 | |
| 147 | ############################################################################### |
| 148 | # exporting that technote should give back similar text |
| 149 | fossil wiki export a3 --technote {2016-01-01 12:34:00} |
| 150 | test wiki-11 {[similar_file f3 a3]} |
| 151 | |
| 152 | ############################################################################### |
| 153 | # Trying to add a technote with the same timestamp should succeed and create a |
| 154 | # second tech note |
| 155 | fossil wiki create 2ndnote f3 -technote {2016-01-01 12:34} |
| 156 | test wiki-13 {$CODE == 0} |
| 157 | fossil wiki list --technote |
| 158 | set technotelist [split $RESULT "\n"] |
| 159 | test wiki-13.1 {[llength $technotelist] == 2} |
| 160 | |
| 161 | ############################################################################### |
| 162 | # commiting a change to an existing technote should replace the page on export |
| 163 | # (this should update th rt |
| 164 | # (this should update the tech note from wiki-13 as that the most recently |
| 165 | # updated one, that should also be the one exported by the export command) |
| 166 | write_file f4 "technote 2nd variant" |
| 167 | fossil wiki commit technote f4 --technote {2016-01-01 12:34} |
| 168 | test wiki-14 {$CODE == 0} |
| 169 | fossil wiki export a4 --technote {2016-01-01 12:34} |
| 170 | test wiki-15 {[similar_file f4 a4]} |
| 171 | # Also check that the tech note with the same timestamp, but modified less |
| 172 | # recently still has its original text |
| 173 | fossil wiki export a4.1 --technote $veryfirsttechnoteid |
| 174 | test wiki-15.1 {[similar_file f3 a4.1]} |
| 175 | |
| 176 | ############################################################################### |
| 177 | # But we shouldn't be able to update non-existant pages |
| 178 | fossil wiki commit doesntexist f1 -expectError |
| 179 | test wiki-16 {$CODE != 0} |
| 180 | |
| 181 | ############################################################################### |
| 182 | # Check specifying tags for a technote is OK |
| 183 | write_file f5 "technote with tags" |
| 184 | fossil wiki create {tagged technote} f5 --technote {2016-01-02 12:34} --technote-tags {A B} |
| 185 | test wiki-17 {$CODE == 0} |
| 186 | write_file f5.1 "editted and tagged technote" |
| 187 | fossil wiki commit {tagged technote} f5 --technote {2016-01-02 12:34} --t note {2016-01 -03 12:34} --technote-bgcolor blue |
| 188 | test wiki-28 {$CODE == 0} |
| 189 | |
| 190 | ############################################################################### |
| 191 | # _file f7 "Different timestamps" |
| 192 | fossil wiki create technotenow f7 --technote {2016-01-04 12:34:56+00:00} |
| 193 | test wiki-29 {$CODE == 0} |
| 194 | |
| 195 | ############################################################################### |
| 196 | # Check a technote appears on the timeline |
| 197 | write_file f8 "Contents of a 'unique' tech note" |
| 198 | fossil wiki create {Unique technote} f8 --technote {2016-01-05 01:02:03} |
| 199 | fossil timeline |
| 200 | test wiki-30 {[string match *Unique*technote* $RESULT]} |
| 201 | |
| 202 | ############################################################################### |
| 203 | # Check for a collision between an attachment and a note, this was a |
| 204 | # bug that resulted from some code treating the attachment entry as if it |
| 205 | # were a technote when it isn't really. |
| 206 | # |
| 207 | # First, wait for the top of the next second so the attachment |
| 208 | # happens at a known time, then add an attachment to an existing note |
| 209 | # and a new note immediately after. |
| 210 | |
| 211 | set t0 [clock seconds] |
| 212 | while {$t0 == [clock seconds]} { |
| 213 | after 100 |
| 214 | } |
| 215 | set t1 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] |
| 216 | write_fil -%m-%d %H:%M:%S"] |
| 217 | write_file f9 "Timestamp: $t1" |
| 218 | fossil attachment add f9 --technote {2016-01-05 01:02:03} |
| 219 | test wiki-31 {$CODE == 0} |
| 220 | fossil wiki create {A CODE == 0} |
| 221 | # |
| 222 | # Now waste time until the next second so that the remaining tests |
| 223 | # don't have to worry about a potential collision |
| 224 | set t0 [clock seconds] |
| 225 | while {$t0 == [clock seconds]} { |
| 226 | after 100 |
| 227 | } |
| 228 | |
| 229 | ############################################################################### |
| 230 | # Check a technote with no timestamp cannot be created, but that |
| 231 | # "now " is a valid stamp. |
| 232 | set t2 [clock format [clock seconds] -gmt 1 -format # Copyright (c) 2016 D. Richard Hiiotelist [llength $technotelified timest ki create technotenow f7 --technote {2016-01-04 12:34:56+00:00} |
| 233 | test wiki-29 {$CODE == 0} |
| 234 | |
| 235 | ############################################################################### |
| 236 | # Check a technote appears on the timeline |
| 237 | write_file f8 "Contents of a 'unique' tech note" |
| 238 | fossil wiki create {Unique technote} f8 --technote {2016-01-05 01:02:03} |
| 239 | fossil timeline |
| 240 | test wiki-30 {[string match *Unique*technote* $RESULT]} |
| 241 | |
| 242 | ############################################################################### |
| 243 | # Check for a collision between an attachment and a note, this was a |
| 244 | # bug that resulted from some code treating the attachment entry as if it |
| 245 | # were a technote when it isn't really. |
| 246 | # |
| 247 | # First, wait for the top of the next second so the attachment |
| 248 | # happens at a known time, then add an attachment to an existing note |
| 249 | # and a new note immediately after. |
| 250 | |
| 251 | set t0 [clock seconds] |
| 252 | while {$t0 == [clock seconds]} { |
| 253 | after 100 |
| 254 | } |
| 255 | set t1 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] |
| 256 | write_file f9 "Timestamp: $t1" |
| 257 | fossil attachment add f9 --technote {2016-01-05 01:02:03} |
| 258 | test wiki-31 {$CODE == 0} |
| 259 | fossil wiki create {Attachment collision} f9 --technote now |
| 260 | test wiki-32 {$CODE == 0} |
| 261 | # |
| 262 | # Now waste time until the next second so that the remaining tests |
| 263 | # don't have to worry about a potential collision |
| 264 | set t0 [clock seconds] |
| 265 | while {$t0 == [clock seconds]} { |
| 266 | after 100 |
| 267 | } |
| 268 | |
| 269 | ############################################################################### |
| 270 | # Check a technote with no timestamp cannot be created, but that |
| 271 | # "now" is a valid stamp. |
| 272 | s et t2 [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d %H:%M:%S"] |
| 273 | write_file f10 "Even unstampted notes are delivered.\nStamped $t2" |
| 274 | fossil wiki create "Unstamped Note" f10 --technote -expectError |
| 275 | test wiki-33 {$CODE != 0} |
| 276 | fossil wiki create "Unstamped Note" f10 --technote now |
| 277 | test wiki-34 {$CODE == 0} |
| 278 | fossil wiki list -t |
| 279 | test wiki-35 {[string match "*$t2*" $RESULT]} |
| 280 | |
| 281 | ############################################################################### |
| 282 | # Check an attachment to it in the same second works. |
| 283 | write_file f11 "Time Stamp was $t2" |
| 284 | fossil attachment add f11 --technote $t2 |
| 285 | test wiki-36 {$CODE == 0} |
| 286 | fossil timeline |
| 287 | test wiki-36-1 {$CODE == 0} |
| 288 | fossil wiki list -t |
| 289 | test wiki-36-2 {$CODE == 0} |
| 290 | |
| 291 | ########################################################################### #technotelist [split $RESULT "\n"] |
| 292 | for {set i 0} {$i < [llength $technotelist]} {incr i} { |
| 293 | set fullid [lindex $technotelist $i] |
| 294 | set id [string range $fullid 0 3] |
| 295 | dict incr idcounts $id |
| 296 | if {[dict get $idcounts $id] > $maxcount} { |
| 297 | set maxid $id |
| 298 | incr maxcount |
| 299 | } |
| 300 | } |
| 301 | # get i so that, as a julian date, it is in the 1800s, i.e., older than |
| 302 | # any other tech note, but after 1 AD |
| 303 | set i 2400000 |
| 304 | while {$maxcount < 2} { |
| 305 | # keep getting older |
| 306 | incr i -1 |
| 307 | write_file f13 "A tech note with timestamp of jday=$i" |
| 308 | fossil wiki create "timestamp of $i" f13 --technote "$i" |
| 309 | fossil wiki list --technote --show-technote-ids |
| 310 | set technotelist [split $RESULT "\n"] |
| 311 | set oldesttechnoteid [lindex [split [lindex $technotelist [llength $technotelist]-1]] 0] |
| 312 | set id [string range $oldesttechnoteid 0 3] |
| 313 | dict incr idcounts $id |
| 314 | if {[dict get $idcounts $id] > $maxcount} { |
| 315 | set maxid $id |
| 316 | incr maxcount |
| 317 | } |
| 318 | } |
| 319 | # Save the duplicate id for this and later tests |
| 320 | set duplicateid $maxid |
| 321 | fossil wiki export a13 --technote $duplicateid -expectError |
| 322 | test wiki-42 {$CODE != 0} |
| 323 | |
| 324 | ############################################################################### |
| 325 | # Check we can update technote by its id |
| 326 | write_file f14 "Updated text for the really old tech note" |
| 327 | fossil wiki commit {Old tech note} f14 --technote $anoldtechnoteid |
| 328 | fossil wiki export a14 --technote $anoldtechnoteid |
| 329 | test wiki-43 {[similar_file f14 a14]} |
| 330 | |
| 331 | ############################################################################### |
| 332 | # Check we can add attachments to a technote by its id |
| 333 | fossil attachment add fa --technote $anoldtechnoteid |
| 334 | test |